├── .editorconfig ├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── main.yaml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── config ├── config.exs └── test.exs ├── lib ├── ueberauth │ └── strategy │ │ ├── auth0.ex │ │ └── auth0 │ │ └── oauth.ex └── ueberauth_auth0.ex ├── mix.exs ├── mix.lock ├── priv ├── .gitkeep └── plts │ └── .gitkeep └── test ├── fixtures ├── auth0.json └── vcr_cassettes │ ├── auth0-invalid-code.json │ ├── auth0-no-access-token.json │ ├── auth0-ok-response.json │ ├── auth0-token-doesnt-expire.json │ ├── auth0-userinfo-invalid-access-token.json │ └── auth0-userinfo-with-errors-in-body.json ├── strategy ├── auth0 │ └── oauth_test.exs └── auth0_test.exs └── test_helper.exs /.editorconfig: -------------------------------------------------------------------------------- 1 | # Check http://editorconfig.org for more information 2 | # This is the main config file for this project: 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = spaces 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "mix" # See documentation for possible values 7 | directory: "/" # Location of package manifests 8 | schedule: 9 | interval: "weekly" 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: tests 8 | 9 | env: 10 | MIX_ENV: test 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-20.04 15 | name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | otp: ["20.3", "21.3", "22.3", "23.3", "24.3", "25.2"] 20 | elixir: ["1.10.4", "1.11.4", "1.12.3", "1.13.4", "1.14.3"] 21 | exclude: 22 | # Supporter Erlang/OTP versions 23 | # Elixir 1.10 ; erlang 21 - 22 (23 from v1.10.3) 24 | - otp: "20.3" 25 | elixir: "1.10.4" 26 | - otp: "24.3" 27 | elixir: "1.10.4" 28 | - otp: "25.2" 29 | elixir: "1.10.4" 30 | - otp: "23.3" 31 | elixir: "1.10.4" 32 | # Elixir 1.11 ; erlang 21 - 23 (24 from v1.11.4) 33 | - otp: "20.3" 34 | elixir: "1.11.4" 35 | - otp: "25.2" 36 | elixir: "1.11.4" 37 | # Elixir 1.12 ; erlang 22 - 24 38 | - otp: "20.3" 39 | elixir: "1.12.3" 40 | - otp: "21.3" 41 | elixir: "1.12.3" 42 | - otp: "25.2" 43 | elixir: "1.12.3" 44 | # Elixir 1.13 ; erlang 22 - 24 (25 from 1.13.4) 45 | - otp: "20.3" 46 | elixir: "1.13.4" 47 | - otp: "21.3" 48 | elixir: "1.13.4" 49 | # Elixir 1.14 ; erlang 23 - 25 50 | - otp: "20.3" 51 | elixir: "1.14.3" 52 | - otp: "21.3" 53 | elixir: "1.14.3" 54 | - otp: "22.3" 55 | elixir: "1.14.3" 56 | env: 57 | MIX_ENV: test 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | steps: 60 | - uses: actions/checkout@v2 61 | - uses: erlef/setup-beam@v1 62 | with: 63 | otp-version: ${{matrix.otp}} 64 | elixir-version: ${{matrix.elixir}} 65 | - name: Cache deps 66 | uses: actions/cache@v3 67 | with: 68 | path: deps 69 | key: ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 70 | restore-keys: ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }}- 71 | - name: Cache build 72 | uses: actions/cache@v3 73 | with: 74 | path: _build 75 | key: ${{ runner.os }}-build-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 76 | restore-keys: ${{ runner.os }}-build-${{ matrix.otp }}-${{ matrix.elixir }}- 77 | - run: mix deps.get 78 | - run: mix compile --warnings-as-errors 79 | - run: mix credo --strict 80 | - run: mix coveralls.github 81 | - run: mix dialyzer 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ueberauth_auth0-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp/ 27 | 28 | # Ignore dialyzer files. 29 | /priv/plts/*.plt 30 | /priv/plts/*.plt.hash 31 | 32 | # Misc. 33 | .elixir_ls 34 | *.code-workspace 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | - 2022-10-28 Dropped support for Elixir < 1.10. Dependencies (`:telemetry` and `:mime`) would not compile. 10 | 11 | ## v2.1.0 - 2022-10-27 12 | - Support for `organization` and `invitation` query parameters 13 | - Bumped dependencies 14 | 15 | ## v2.0.0 - 2021-08-14 16 | 17 | - BREAKING CHANGE: changed error management on wrong OAuth code. 18 | Instead of raising a `OAuth2.Error`, the `conn.assigns.ueberauth_failure` 19 | is set with the following value: 20 | ```elixir 21 | %Ueberauth.Failure.Error{ 22 | message: "Invalid authorization code", 23 | message_key: "invalid_grant" 24 | } 25 | ``` 26 | - Bumped dependencies 27 | 28 | ## v1.0.0 - 2020-07-10 29 | 30 | - BREAKING CHANGE: bump `ueberauth` to `0.7.0` which provides default CSRF protection. 31 | In exchange for this new default protection, the `state` field is used by `ueberauth` 32 | to store the CSRF token. 33 | - Documentation improvements 34 | 35 | ## v0.8.1 - 2020-08-18 36 | 37 | - Adds query parameters used for the Universal Login: `screen_hint`, 38 | `login_hint` and `prompt`. 39 | See https://auth0.com/docs/universal-login/new-experience#signup 40 | 41 | ## v0.8.0 - 2020-08-18 42 | 43 | - BREAKING CHANGE: the `%Extra{}` field now copies the full raw auth0 user into 44 | `%Extra{raw_info: %{user: auth0_user}}` instead of selected fields. This 45 | allows better usage with custom auth0 fields and other end-user customizations. 46 | (see PR #136) 47 | - The `%Extra{}` field now also contains the raw auth0 token (if you ever 48 | need it) under `:token` in the `raw_info` map. This better follows other ueberauth 49 | strategies and can be useful in some cases. 50 | - Bump dependencies 51 | 52 | ## v0.7.0 - 2020-07-30 53 | 54 | - Changes in the accepted params that can be given to the 55 | `:request` endpoint: `audience`, `state`, `connection` and 56 | `scope`. Corresponding default values have been added to the 57 | configuration options. 58 | - Improved error message on missing configuration. 59 | 60 | ## v0.6.0 - 2020-07-27 61 | 62 | - Adds `%Extra{}` data with all fields from `/userinfo` mapped. 63 | - BREAKING CHANGE: `locale` data is now stored in the `%Extra{}` 64 | field instead of the `%Info{location: ...}` field 65 | - Bumped dependencies (earmark) 66 | 67 | ## v0.5.0 - 2020-07-24 68 | 69 | - Drops support for Elixir 1.4, 1.5 and 1.6 70 | - Adds integration tests suite 71 | - `deps` update 72 | 73 | ## v0.4.0 - 2019-10-05 74 | 75 | - Massive `deps` update, mainly `oauth2`. 76 | 77 | ## v0.3.0 - 2017-09-17 78 | 79 | Thanks @sobolevn for updating the followings: 80 | 81 | - Changed the `uid_field` to `sub` to match with the return from Auth0. 82 | - Included `profile` scope by default. 83 | 84 | ## v0.2.0 - 2017-05-15 85 | 86 | - Initial semantic release 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ueberauth_auth0 2 | 3 | ## Pull Requests Welcome 4 | 5 | 1. Fork `ueberauth_auth0` 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_auth0/master` 10 | 11 | ## Development 12 | 13 | Please, make sure that all these commands succeed before pushing anything: 14 | 15 | 1. `mix test` 16 | 2. `mix credo --strict` 17 | 3. `mix dialyzer` (it may take long on the first run) 18 | 19 | ## Issues 20 | 21 | If you believe there to be a bug, please provide the maintainers with enough 22 | detail to reproduce or a link to an app exhibiting unexpected behavior. For 23 | help, please start with Stack Overflow. 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Son Tran-Nguyen \ 4 | Copyright (c) 2020 Klemen Sever 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Überauth Auth0 2 | 3 | [![Build Status](https://github.com/achedeuzot/ueberauth_auth0/workflows/tests/badge.svg)](https://github.com/achedeuzot/ueberauth_auth0/actions?query=workflow%3Atests+branch%3Amaster) 4 | [![Coverage Status](https://coveralls.io/repos/github/achedeuzot/ueberauth_auth0/badge.svg?branch=master)](https://coveralls.io/github/achedeuzot/ueberauth_auth0?branch=master) 5 | [![Module Version](https://img.shields.io/hexpm/v/ueberauth_auth0.svg)](https://hex.pm/packages/ueberauth_auth0) 6 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ueberauth_auth0/) 7 | [![Total Download](https://img.shields.io/hexpm/dt/ueberauth_auth0.svg)](https://hex.pm/packages/ueberauth_auth0) 8 | [![License](https://img.shields.io/hexpm/l/ueberauth_auth0.svg)](https://github.com/achedeuzot/ueberauth_auth0/blob/master/LICENSE.md) 9 | [![Last Updated](https://img.shields.io/github/last-commit/achedeuzot/ueberauth_auth0.svg)](https://github.com/achedeuzot/ueberauth_auth0/commits/master) 10 | 11 | > Auth0 OAuth2 strategy for Überauth. 12 | 13 | ## Installation 14 | 15 | 1. Set up your Auth0 application at [Auth0 dashboard](https://manage.auth0.com/#/applications). 16 | 17 | 2. Add `:ueberauth_auth0` to your list of dependencies in `mix.exs`: 18 | 19 | ```elixir 20 | def deps do 21 | [ 22 | {:ueberauth_auth0, "~> 2.0"} 23 | ] 24 | end 25 | ``` 26 | 27 | 3. Ensure `ueberauth_auth0` is started before your application: 28 | 29 | ```elixir 30 | def application do 31 | [ 32 | applications: [:ueberauth_auth0] 33 | ] 34 | end 35 | ``` 36 | 37 | 4. Add Auth0 to your Überauth configuration: 38 | 39 | ```elixir 40 | config :ueberauth, Ueberauth, 41 | providers: [ 42 | auth0: {Ueberauth.Strategy.Auth0, []} 43 | ], 44 | # If you wish to customize the OAuth serializer, 45 | # add the line below. Defaults to Jason. 46 | json_library: Poison 47 | ``` 48 | 49 | **or** with per-app config: 50 | 51 | ```elixir 52 | config :my_app, Ueberauth, 53 | providers: [ 54 | auth0: {Ueberauth.Strategy.Auth0, [otp_app: :my_app]} 55 | ] 56 | ``` 57 | 58 | 5. Update your provider configuration: 59 | 60 | ```elixir 61 | config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, 62 | domain: System.get_env("AUTH0_DOMAIN"), 63 | client_id: System.get_env("AUTH0_CLIENT_ID"), 64 | client_secret: System.get_env("AUTH0_CLIENT_SECRET") 65 | ``` 66 | 67 | **or** with per-app config: 68 | 69 | ```elixir 70 | config :my_app, Ueberauth.Strategy.Auth0.OAuth, 71 | domain: System.get_env("AUTH0_DOMAIN"), 72 | client_id: System.get_env("AUTH0_CLIENT_ID"), 73 | client_secret: System.get_env("AUTH0_CLIENT_SECRET") 74 | ``` 75 | 76 | See the `Ueberauth.Strategy.Auth0` module docs for more 77 | configuration options. 78 | 79 | 6. Include the Überauth plug in your controller: 80 | 81 | ```elixir 82 | defmodule MyApp.AuthController do 83 | use MyApp.Web, :controller 84 | plug Ueberauth 85 | ... 86 | end 87 | ``` 88 | 89 | **or** with per-app config: 90 | 91 | ```elixir 92 | defmodule MyApp.AuthController do 93 | use MyApp.Web, :controller 94 | plug Ueberauth, otp_app: :my_app 95 | ... 96 | end 97 | ``` 98 | 99 | 7. Create the request and callback routes if you haven't already: 100 | 101 | ```elixir 102 | scope "/auth", MyApp do 103 | pipe_through :browser 104 | 105 | get "/:provider", AuthController, :request 106 | get "/:provider/callback", AuthController, :callback 107 | end 108 | ``` 109 | 110 | 8. You controller needs to implement callbacks to deal with Ueberauth.Auth and Ueberauth.Failure responses. 111 | 112 | For an example implementation see the [Überauth Example](https://github.com/ueberauth/ueberauth_example) application. 113 | 114 | ## Learn about OAuth2 115 | [OAuth2 explained with cute shapes](https://engineering.backmarket.com/oauth2-explained-with-cute-shapes-7eae51f20d38) 116 | 117 | ## Copyright and License 118 | 119 | Copyright (c) 2015 Son Tran-Nguyen \ 120 | Copyright (c) 2020 Klemen Sever 121 | 122 | This library is released under the MIT License. See the [LICENSE.md](./LICENSE.md) file 123 | for further details. 124 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Instructions 2 | 3 | 1. Bump version in related files below 4 | 2. Run tests: 5 | - `mix test` in the root folder 6 | 3. Commit, push code 7 | 4. Publish package and docs after pruning any extraneous uncommitted files 8 | 5. Create a release on GitHub or push a tag with `git tag -a v1.0` && `git push origin v1.0` 9 | 10 | ## Files with version 11 | 12 | * `mix.exs` 13 | * `README.md` (Installation section) 14 | * `CHANGELOG.md` 15 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :ueberauth_auth0, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:ueberauth_auth0, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | if Mix.env() == :test, do: import_config("test.exs") 32 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ueberauth, Ueberauth, 4 | json_library: Jason, 5 | providers: [ 6 | auth0: {Ueberauth.Strategy.Auth0, []} 7 | ] 8 | 9 | config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, 10 | domain: "example-app.auth0.com", 11 | client_id: "clientidsomethingrandom", 12 | client_secret: "clientsecret-somethingsecret" 13 | 14 | config :exvcr, 15 | vcr_cassette_library_dir: "test/fixtures/vcr_cassettes" 16 | 17 | config :plug, :validate_header_keys_during_test, true 18 | -------------------------------------------------------------------------------- /lib/ueberauth/strategy/auth0.ex: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Auth0 do 2 | @moduledoc """ 3 | Provides an Ueberauth strategy for authenticating with Auth0. 4 | 5 | You can edit the behaviour of the Strategy by including some options when 6 | you register your provider. 7 | 8 | To set the `uid_field` 9 | config :ueberauth, Ueberauth, 10 | providers: [ 11 | auth0: { Ueberauth.Strategy.Auth0, [uid_field: :email] } 12 | ] 13 | Default is `:sub` 14 | 15 | To set the default ['scope'](https://auth0.com/docs/scopes) (permissions): 16 | config :ueberauth, Ueberauth, 17 | providers: [ 18 | auth0: { Ueberauth.Strategy.Auth0, [default_scope: "openid profile email"] } 19 | ] 20 | Default is `"openid profile email"`. 21 | 22 | To set the [`audience`](https://auth0.com/docs/glossary#audience) 23 | config :ueberauth, Ueberauth, 24 | providers: [ 25 | auth0: { Ueberauth.Strategy.Auth0, [default_audience: "example-audience"] } 26 | ] 27 | Not used by default (set to `""`). 28 | 29 | To set the [`connection`](https://auth0.com/docs/identityproviders), mostly useful if 30 | you want to use a social identity provider like `facebook` or `google-oauth2`. If empty 31 | it will redirect to Auth0's Login widget. See https://auth0.com/docs/api/authentication#social 32 | config :ueberauth, Ueberauth, 33 | providers: [ 34 | auth0: { Ueberauth.Strategy.Auth0, [default_connection: "facebook"] } 35 | ] 36 | Not used by default (set to `""`) 37 | 38 | To set the [`state`](https://auth0.com/docs/protocols/oauth2/oauth-state). This is useful 39 | to prevent from CSRF attacks and redirect users to the state before the authentication flow 40 | started. 41 | config :ueberauth, Ueberauth, 42 | providers: [ 43 | auth0: { Ueberauth.Strategy.Auth0, [default_state: "some-opaque-state"] } 44 | ] 45 | Not used by default (set to `""`) 46 | 47 | These 4 parameters can also be set in the request to authorization. e.g. 48 | You can call the `auth0` authentication endpoint with values: 49 | `/auth/auth0?scope="some+new+scope&audience=events:read&connection=facebook&state=opaque_value` 50 | 51 | ## About the `state` param 52 | Usually a static `state` value is not very useful so it's best to pass it to 53 | the request endpoint as a parameter. You can then read back the state after 54 | authentication in a private value set in the connection: `auth0_state`. 55 | 56 | ### Example 57 | 58 | state_signed = Phoenix.Token.sign(MyApp.Endpoint, "return_url", Phoenix.Controller.current_url(conn)) 59 | Routes.auth_path(conn, :request, "auth0", state: state_signed) 60 | # authentication happens ... 61 | # the state ends up in `conn.private.auth0_state` after the authentication process 62 | {:ok, redirect_to} = Phoenix.Token.verify(MyApp.Endpoint, "return_url", conn.private.auth0_state, max_age: 900) 63 | 64 | """ 65 | use Ueberauth.Strategy, 66 | uid_field: :sub, 67 | default_scope: "openid profile email", 68 | default_audience: "", 69 | default_connection: "", 70 | default_prompt: "", 71 | default_screen_hint: "", 72 | default_login_hint: "", 73 | default_organization: "", 74 | # See https://auth0.com/docs/manage-users/organizations/configure-organizations/invite-members#specify-route-behavior 75 | default_invitation: "", 76 | allowed_request_params: [ 77 | :scope, 78 | :state, 79 | :audience, 80 | :connection, 81 | :prompt, 82 | :screen_hint, 83 | :login_hint, 84 | :organization, 85 | :invitation 86 | ], 87 | oauth2_module: Ueberauth.Strategy.Auth0.OAuth 88 | 89 | alias OAuth2.{Client, Error, Response} 90 | alias Plug.Conn 91 | alias Ueberauth.Auth.{Credentials, Extra, Info} 92 | 93 | @doc """ 94 | Handles the redirect to Auth0. 95 | """ 96 | def handle_request!(conn) do 97 | allowed_params = 98 | conn 99 | |> option(:allowed_request_params) 100 | |> Enum.map(&to_string/1) 101 | 102 | opts = 103 | conn.params 104 | |> maybe_replace_param(conn, "scope", :default_scope) 105 | |> maybe_replace_param(conn, "audience", :default_audience) 106 | |> maybe_replace_param(conn, "connection", :default_connection) 107 | |> maybe_replace_param(conn, "prompt", :default_prompt) 108 | |> maybe_replace_param(conn, "screen_hint", :default_screen_hint) 109 | |> maybe_replace_param(conn, "login_hint", :default_login_hint) 110 | |> maybe_replace_param(conn, "organization", :default_organization) 111 | |> maybe_replace_param(conn, "invitation", :default_invitation) 112 | |> Map.put("state", conn.private[:ueberauth_state_param]) 113 | |> Enum.filter(fn {k, _} -> Enum.member?(allowed_params, k) end) 114 | # Remove empty params 115 | |> Enum.reject(fn {_, v} -> blank?(v) end) 116 | |> Enum.map(fn {k, v} -> {String.to_existing_atom(k), v} end) 117 | |> Keyword.put(:redirect_uri, callback_url(conn)) 118 | 119 | module = option(conn, :oauth2_module) 120 | 121 | callback_url = module.authorize_url!(opts, [otp_app: option(conn, :otp_app)]) 122 | 123 | redirect!(conn, callback_url) 124 | end 125 | 126 | @doc """ 127 | Handles the callback from Auth0. When there is a failure from Auth0 the failure is included in the 128 | `ueberauth_failure` struct. Otherwise the information returned from Auth0 is returned in the `Ueberauth.Auth` struct. 129 | """ 130 | def handle_callback!(%Conn{params: %{"code" => _}} = conn) do 131 | {code, state} = parse_params(conn) 132 | module = option(conn, :oauth2_module) 133 | redirect_uri = callback_url(conn) 134 | 135 | result = module.get_token!([code: code, redirect_uri: redirect_uri], [otp_app: option(conn, :otp_app)]) 136 | 137 | case result do 138 | {:ok, client} -> 139 | token = client.token 140 | 141 | if token.access_token == nil do 142 | set_errors!(conn, [ 143 | error( 144 | token.other_params["error"], 145 | token.other_params["error_description"] 146 | ) 147 | ]) 148 | else 149 | fetch_user(conn, client, state) 150 | end 151 | 152 | {:error, client} -> 153 | set_errors!(conn, [error(client.body["error"], client.body["error_description"])]) 154 | end 155 | end 156 | 157 | @doc false 158 | def handle_callback!(conn) do 159 | set_errors!(conn, [error("missing_code", "No code received")]) 160 | end 161 | 162 | @doc """ 163 | Cleans up the private area of the connection used for passing the raw Auth0 response around during the callback. 164 | """ 165 | def handle_cleanup!(conn) do 166 | conn 167 | |> put_private(:auth0_user, nil) 168 | |> put_private(:auth0_token, nil) 169 | end 170 | 171 | defp fetch_user(conn, %{token: token} = client, state) do 172 | conn = 173 | conn 174 | |> put_private(:auth0_token, token) 175 | |> put_private(:auth0_state, state) 176 | 177 | case Client.get(client, "/userinfo") do 178 | {:ok, %Response{status_code: status_code, body: user}} 179 | when status_code in 200..399 -> 180 | put_private(conn, :auth0_user, user) 181 | 182 | {:error, %Response{status_code: 401, body: _body}} -> 183 | set_errors!(conn, [error("OAuth2", "unauthorized_token")]) 184 | 185 | {:error, %Response{body: body}} -> 186 | set_errors!(conn, [error("OAuth2", body)]) 187 | 188 | {:error, %Error{reason: reason}} -> 189 | set_errors!(conn, [error("OAuth2", reason)]) 190 | end 191 | end 192 | 193 | @doc """ 194 | Fetches the uid field from the Auth0 response. 195 | """ 196 | def uid(conn) do 197 | conn.private.auth0_user[to_string(option(conn, :uid_field))] 198 | end 199 | 200 | @doc """ 201 | Includes the credentials from the Auth0 response. 202 | """ 203 | def credentials(conn) do 204 | token = conn.private.auth0_token 205 | 206 | scopes = 207 | (token.other_params["scope"] || "") 208 | |> String.split(" ") 209 | 210 | %Credentials{ 211 | token: token.access_token, 212 | refresh_token: token.refresh_token, 213 | token_type: token.token_type, 214 | expires_at: token.expires_at, 215 | expires: token_expired(token), 216 | scopes: scopes, 217 | other: token.other_params 218 | } 219 | end 220 | 221 | defp token_expired(%{expires_at: nil}), do: false 222 | defp token_expired(%{expires_at: _}), do: true 223 | 224 | @doc """ 225 | Populates the extra section of the `Ueberauth.Auth` struct with auth0's 226 | additional information from the `/userinfo` user profile and includes the 227 | token received from Auth0 callback. 228 | """ 229 | def extra(conn) do 230 | %Extra{ 231 | raw_info: %{ 232 | token: conn.private.auth0_token, 233 | user: conn.private.auth0_user 234 | } 235 | } 236 | end 237 | 238 | @doc """ 239 | Fetches the fields to populate the info section of the `Ueberauth.Auth` struct. 240 | 241 | This field has been changed from 0.5.0 to 0.6.0 to better reflect 242 | fields of the OpenID standard claims. Extra fields provided by 243 | auth0 are in the `Extra` struct. 244 | """ 245 | def info(conn) do 246 | user = conn.private.auth0_user 247 | 248 | %Info{ 249 | name: user["name"], 250 | first_name: user["given_name"], 251 | last_name: user["family_name"], 252 | nickname: user["nickname"], 253 | email: user["email"], 254 | # The `locale` auth0 field has been moved to `Extra` to better follow OpenID standard specs. 255 | # The `location` field of `Ueberauth.Auth.Info` is intended for location (city, country, ...) 256 | # information while the `locale` information returned by auth0 is used for internationalization. 257 | # There is no location field in the auth0 response, only an `address`. 258 | location: nil, 259 | description: nil, 260 | image: user["picture"], 261 | phone: user["phone_number"], 262 | birthday: user["birthdate"], 263 | urls: %{ 264 | profile: user["profile"], 265 | website: user["website"] 266 | } 267 | } 268 | end 269 | 270 | defp parse_params(%Plug.Conn{params: %{"code" => code, "state" => state}}) do 271 | {code, state} 272 | end 273 | 274 | defp parse_params(%Plug.Conn{params: %{"code" => code}}) do 275 | {code, nil} 276 | end 277 | 278 | defp option(conn, key) do 279 | default = Keyword.get(default_options(), key) 280 | 281 | conn 282 | |> options 283 | |> Keyword.get(key, default) 284 | end 285 | 286 | defp option(nil, conn, key), do: option(conn, key) 287 | defp option(value, _conn, _key), do: value 288 | 289 | defp maybe_replace_param(params, conn, name, config_key) do 290 | if params[name] do 291 | params 292 | else 293 | Map.put(params, name, option(params[name], conn, config_key)) 294 | end 295 | end 296 | 297 | @compile {:inline, blank?: 1} 298 | def blank?(""), do: true 299 | def blank?([]), do: true 300 | def blank?(nil), do: true 301 | def blank?({}), do: true 302 | def blank?(%{} = map) when map_size(map) == 0, do: true 303 | def blank?(_), do: false 304 | end 305 | -------------------------------------------------------------------------------- /lib/ueberauth/strategy/auth0/oauth.ex: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Auth0.OAuth do 2 | @moduledoc """ 3 | An implementation of OAuth2 for Auth0. 4 | To add your `domain`, `client_id` and `client_secret` include these values in your configuration. 5 | config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, 6 | domain: System.get_env("AUTH0_DOMAIN"), 7 | client_id: System.get_env("AUTH0_CLIENT_ID"), 8 | client_secret: System.get_env("AUTH0_CLIENT_SECRET") 9 | 10 | Alternatively, if you need to setup config without needing to recompile, do the following. 11 | config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, 12 | domain: {:system, "AUTH0_DOMAIN"}, 13 | client_id: {:system, "AUTH0_CLIENT_ID"}, 14 | client_secret: {:system, "AUTH0_CLIENT_SECRET"} 15 | 16 | The JSON serializer used is the same as `Ueberauth` so if you need to 17 | customize it, you can configure it in the `Ueberauth` configuration: 18 | 19 | config :ueberauth, Ueberauth, 20 | json_library: Poison # Defaults to Jason 21 | 22 | """ 23 | use OAuth2.Strategy 24 | alias OAuth2.Client 25 | alias OAuth2.Strategy.AuthCode 26 | 27 | def options(otp_app) do 28 | configs = Application.get_env(otp_app || :ueberauth, Ueberauth.Strategy.Auth0.OAuth) 29 | 30 | unless configs do 31 | raise( 32 | "Expected to find settings under `config #{inspect(otp_app)}, Ueberauth.Strategy.Auth0.OAuth`, " <> 33 | "got nil. Check your config.exs." 34 | ) 35 | end 36 | 37 | domain = get_config_value(configs[:domain]) 38 | client_id = get_config_value(configs[:client_id]) 39 | client_secret = get_config_value(configs[:client_secret]) 40 | 41 | serializers = %{ 42 | "application/json" => Ueberauth.json_library(otp_app) 43 | } 44 | 45 | opts = [ 46 | strategy: __MODULE__, 47 | site: "https://#{domain}", 48 | authorize_url: "https://#{domain}/authorize", 49 | token_url: "https://#{domain}/oauth/token", 50 | userinfo_url: "https://#{domain}/userinfo", 51 | client_id: client_id, 52 | client_secret: client_secret, 53 | serializers: serializers 54 | ] 55 | 56 | Keyword.merge(configs, opts) 57 | end 58 | 59 | @doc """ 60 | Construct a client for requests to Auth0. 61 | Optionally include any OAuth2 options here to be merged with the defaults. 62 | Ueberauth.Strategy.Auth0.OAuth.client(redirect_uri: "http://localhost:4000/auth/auth0/callback") 63 | This will be setup automatically for you in `Ueberauth.Strategy.Auth0`. 64 | These options are only useful for usage outside the normal callback phase of Ueberauth. 65 | """ 66 | def client(opts \\ []) do 67 | opts 68 | |> Keyword.get(:otp_app) 69 | |> options() 70 | |> Keyword.merge(opts) 71 | |> Client.new() 72 | end 73 | 74 | @doc """ 75 | Provides the authorize url for the request phase of Ueberauth. No need to call this usually. 76 | """ 77 | def authorize_url!(params \\ [], opts \\ []) do 78 | opts 79 | |> client 80 | |> Client.authorize_url!(params) 81 | end 82 | 83 | def get_token!(params \\ [], opts \\ []) do 84 | otp_app = Keyword.get(opts, :otp_app) 85 | 86 | client_secret = 87 | otp_app 88 | |> options() 89 | |> Keyword.get(:client_secret) 90 | 91 | params = Keyword.merge(params, client_secret: client_secret) 92 | headers = Keyword.get(opts, :headers, []) 93 | opts = Keyword.get(opts, :options, []) 94 | 95 | client_options = 96 | opts 97 | |> Keyword.get(:client_options, []) 98 | |> Keyword.merge(otp_app: otp_app) 99 | 100 | Client.get_token(client(client_options), params, headers, opts) 101 | end 102 | 103 | # Strategy Callbacks 104 | 105 | def authorize_url(client, params) do 106 | AuthCode.authorize_url(client, params) 107 | end 108 | 109 | def get_token(client, params, headers) do 110 | client 111 | |> put_header("Accept", "application/json") 112 | |> AuthCode.get_token(params, headers) 113 | end 114 | 115 | defp get_config_value({:system, value}), do: System.get_env(value) 116 | defp get_config_value(value), do: value 117 | end 118 | -------------------------------------------------------------------------------- /lib/ueberauth_auth0.ex: -------------------------------------------------------------------------------- 1 | defmodule UeberauthAuth0 do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule UeberauthAuth0.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/achedeuzot/ueberauth_auth0" 5 | @version "2.1.0" 6 | 7 | def project do 8 | [ 9 | app: :ueberauth_auth0, 10 | version: @version, 11 | name: "Ueberauth Auth0", 12 | package: package(), 13 | elixir: "~> 1.10", 14 | deps: deps(), 15 | docs: docs(), 16 | build_embedded: Mix.env() == :prod, 17 | start_permanent: Mix.env() == :prod, 18 | 19 | # Test coverage: 20 | test_coverage: [tool: ExCoveralls], 21 | preferred_cli_env: [ 22 | coveralls: :test, 23 | "coveralls.detail": :test, 24 | "coveralls.post": :test, 25 | "coveralls.html": :test, 26 | vcr: :test, 27 | "vcr.delete": :test, 28 | "vcr.check": :test, 29 | "vcr.show": :test 30 | ], 31 | 32 | # Type checking 33 | dialyzer: [ 34 | plt_core_path: "_build/#{Mix.env()}" 35 | ] 36 | ] 37 | end 38 | 39 | def application do 40 | [ 41 | extra_applications: [:logger] 42 | ] 43 | end 44 | 45 | defp deps do 46 | [ 47 | {:ueberauth, "~> 0.10"}, 48 | {:oauth2, "~> 2.0"}, 49 | 50 | # Docs: 51 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 52 | 53 | # Testing: 54 | {:exvcr, "~> 0.10", only: :test}, 55 | {:excoveralls, "~> 0.11", only: :test}, 56 | 57 | # Type checking 58 | {:dialyxir, "~> 1.2.0", only: [:dev, :test], runtime: false}, 59 | 60 | # Lint: 61 | {:credo, "~> 1.1", only: [:dev, :test]} 62 | ] 63 | end 64 | 65 | defp docs do 66 | [ 67 | extras: [ 68 | "CHANGELOG.md", 69 | "LICENSE.md": [title: "License"], 70 | "README.md": [title: "Overview"] 71 | ], 72 | source_url: @source_url, 73 | source_ref: "v#{@version}", 74 | main: "readme", 75 | formatters: ["html"] 76 | ] 77 | end 78 | 79 | defp package do 80 | [ 81 | name: :ueberauth_auth0, 82 | description: "An Ueberauth strategy for using Auth0 to authenticate your users.", 83 | files: ["lib", "mix.exs", "README.md", "LICENSE.md"], 84 | maintainers: ["Son Tran-Nguyen", "Nikita Sobolev", "Klemen Sever"], 85 | licenses: ["MIT"], 86 | links: %{ 87 | Changelog: "https://hexdocs.pm/ueberauth_auth0/changelog.html", 88 | GitHub: @source_url 89 | } 90 | ] 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [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", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, 5 | "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, 6 | "earmark": {:hex, :earmark, "1.4.15", "2c7f924bf495ec1f65bd144b355d0949a05a254d0ec561740308a54946a67888", [:mix], [{:earmark_parser, ">= 1.4.13", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "3b1209b85bc9f3586f370f7c363f6533788fb4e51db23aa79565875e7f9999ee"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, 8 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 9 | "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [: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", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, 10 | "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, 11 | "excoveralls": {:hex, :excoveralls, "0.15.3", "54bb54043e1cf5fe431eb3db36b25e8fd62cf3976666bafe491e3fa5e29eba47", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8eb5d8134d84c327685f7bb8f1db4147f1363c3c9533928234e496e3070114e"}, 12 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, 13 | "exvcr": {:hex, :exvcr, "0.13.4", "68efca5ae04a909b29a9e137338a7033642898033c7a938a5faec545bfc5a38e", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "42920a59bdeef34001f8c2305a57d68b29d8a2e7aa1877bb35a75034b9f9904a"}, 14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 15 | "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"}, 16 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 17 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 18 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, 19 | "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"}, 20 | "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"}, 21 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 22 | "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, 23 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 24 | "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, 25 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 27 | "oauth2": {:hex, :oauth2, "2.0.1", "70729503e05378697b958919bb2d65b002ba6b28c8112328063648a9348aaa3f", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "c64e20d4d105bcdbcbe03170fb530d0eddc3a3e6b135a87528a22c8aecf74c52"}, 28 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 29 | "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"}, 30 | "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, 31 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 32 | "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, 33 | "ueberauth": {:hex, :ueberauth, "0.10.3", "4a3bd7ab7b5d93d301d264f0f6858392654ee92171f4437d067d1ae227c051d9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "1394f36a6c64e97f2038cf95228e7e52b4cb75417962e30418fbe9902b30e6d3"}, 34 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 35 | } 36 | -------------------------------------------------------------------------------- /priv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/achedeuzot/ueberauth_auth0/de1964785a3d8dc47fba2eb850345f0028b46651/priv/.gitkeep -------------------------------------------------------------------------------- /priv/plts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/achedeuzot/ueberauth_auth0/de1964785a3d8dc47fba2eb850345f0028b46651/priv/plts/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/auth0.json: -------------------------------------------------------------------------------- 1 | { 2 | "sub": "auth0|lyy5v452u345tbn943qf", 3 | "name": "Jane Josephine Doe", 4 | "given_name": "Jane", 5 | "family_name": "Doe", 6 | "middle_name": "Josephine", 7 | "nickname": "JJ", 8 | "preferred_username": "j.doe", 9 | "profile": "http://example.com/janedoe", 10 | "picture": "http://example.com/janedoe/me.jpg", 11 | "website": "http://example.com", 12 | "email": "janedoe@example.com", 13 | "email_verified": true, 14 | "gender": "female", 15 | "birthdate": "1972-03-31", 16 | "zoneinfo": "America/Los_Angeles", 17 | "locale": "en-US", 18 | "phone_number": "+1 (111) 222-3434", 19 | "phone_number_verified": false, 20 | "address": { 21 | "country": "us" 22 | }, 23 | "updated_at": "1556845729" 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/auth0-invalid-code.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "client_id=clientidKFpqmR7aO1iVUOQR1LMKbZvk&client_secret=clientsecrethzqAhvWpJhAehfkzupWelgut-hRh7ZYOBOmaXsCRryhy7bqWzOCx&code=invalid_code&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback", 5 | "headers": { 6 | "content-type": "application/x-www-form-urlencoded", 7 | "accept": "application/json", 8 | "authorization": "Basic Y2xpZW50aWRLRnBxbVI3YU8xaVZVT1FSMUxNS2Jadms6Y2xpZW50c2VjcmV0aHpxQWh2V3BKaEFlaGZrenVwV2VsZ3V0LWhSaDdaWU9CT21hWHNDUnJ5aHk3YnFXek9DeA==" 9 | }, 10 | "method": "post", 11 | "options": [], 12 | "request_body": "", 13 | "url": "https://example-app.auth0.com/oauth/token" 14 | }, 15 | "response": { 16 | "binary": false, 17 | "body": "{\"error\":\"invalid_grant\",\"error_description\":\"Invalid authorization code\"}", 18 | "headers": { 19 | "Date": "Thu, 18 Jun 2020 07:03:26 GMT", 20 | "Content-Type": "application/json", 21 | "Content-Length": "74", 22 | "Connection": "keep-alive", 23 | "Server": "nginx", 24 | "Vary": "Accept-Encoding", 25 | "ot-tracer-spanid": "<>", 26 | "ot-tracer-traceid": "<>", 27 | "ot-tracer-sampled": "true", 28 | "ot-baggage-auth0-request-id": "<>", 29 | "X-Auth0-RequestId": "<>", 30 | "Set-Cookie": "<>", 31 | "X-RateLimit-Limit": "30", 32 | "X-RateLimit-Remaining": "29", 33 | "X-RateLimit-Reset": "1592463807", 34 | "cache-control": "private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform", 35 | "Strict-Transport-Security": "max-age=15724800" 36 | }, 37 | "status_code": 403, 38 | "type": "ok" 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/auth0-no-access-token.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "client_id=clientidKFpqmR7aO1iVUOQR1LMKbZvk&client_secret=clientsecrethzqAhvWpJhAehfkzupWelgut-hRh7ZYOBOmaXsCRryhy7bqWzOCx&code=some_code&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback", 5 | "headers": { 6 | "content-type": "application/x-www-form-urlencoded", 7 | "accept": "application/json", 8 | "authorization": "Basic Y2xpZW50aWRLRnBxbVI3YU8xaVZVT1FSMUxNS2Jadms6Y2xpZW50c2VjcmV0aHpxQWh2V3BKaEFlaGZrenVwV2VsZ3V0LWhSaDdaWU9CT21hWHNDUnJ5aHk3YnFXek9DeA==" 9 | }, 10 | "method": "post", 11 | "options": [], 12 | "request_body": "", 13 | "url": "https://example-app.auth0.com/oauth/token" 14 | }, 15 | "response": { 16 | "binary": false, 17 | "body": "{\"error\":\"something_wrong\",\"error_description\":\"Something went wrong\"}", 18 | "headers": { 19 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 20 | "Content-Type": "application/json", 21 | "Content-Length": "69", 22 | "Connection": "keep-alive", 23 | "Server": "nginx", 24 | "ot-tracer-spanid": "<>", 25 | "ot-tracer-traceid": "<>", 26 | "ot-tracer-sampled": "true", 27 | "ot-baggage-auth0-request-id": "<>", 28 | "X-Auth0-RequestId": "<>", 29 | "Set-Cookie": "<>", 30 | "X-RateLimit-Limit": "30", 31 | "X-RateLimit-Remaining": "29", 32 | "X-RateLimit-Reset": "1592437177", 33 | "cache-control": "private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform" 34 | }, 35 | "status_code": 200, 36 | "type": "ok" 37 | } 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/auth0-ok-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "client_id=clientidKFpqmR7aO1iVUOQR1LMKbZvk&client_secret=clientsecrethzqAhvWpJhAehfkzupWelgut-hRh7ZYOBOmaXsCRryhy7bqWzOCx&code=code_abc&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback", 5 | "headers": { 6 | "content-type": "application/x-www-form-urlencoded", 7 | "accept": "application/json", 8 | "authorization": "Basic Y2xpZW50aWRLRnBxbVI3YU8xaVZVT1FSMUxNS2Jadms6Y2xpZW50c2VjcmV0aHpxQWh2V3BKaEFlaGZrenVwV2VsZ3V0LWhSaDdaWU9CT21hWHNDUnJ5aHk3YnFXek9DeA==" 9 | }, 10 | "method": "post", 11 | "options": [], 12 | "request_body": "", 13 | "url": "https://example-app.auth0.com/oauth/token" 14 | }, 15 | "response": { 16 | "binary": false, 17 | "body": "{\"access_token\":\"eyJz93alolk4laUWw\",\"refresh_token\":\"GEbRxBNkitedjnXbL\",\"id_token\":\"eyJ0XAipop4faeEoQ\",\"token_type\":\"Bearer\",\"expires_in\":86400,\"scope\":\"openid profile email\"}", 18 | "headers": { 19 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 20 | "Content-Type": "application/json", 21 | "Content-Length": "144", 22 | "Connection": "keep-alive", 23 | "X-Auth0-RequestId": "<>" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | }, 29 | { 30 | "request": { 31 | "body": "", 32 | "headers": { 33 | "accept": "application/json", 34 | "authorization": "Bearer eyJz93alolk4laUWw" 35 | }, 36 | "method": "get", 37 | "options": [], 38 | "request_body": "", 39 | "url": "https://example-app.auth0.com/userinfo" 40 | }, 41 | "response": { 42 | "binary": false, 43 | "body": "{\"email\":\"testuser@example.com\",\"email_verified\":false,\"name\":\"testuser@example.com\",\"nickname\":\"testuser\",\"picture\":\"https://s.gravatar.com/avatar/7ec7606c46a14a7ef514d1f1f9038823?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Ftu.png\",\"sub\":\"auth0|lyy5v5utb6n9qfm4ihi3l7pv34po66\",\"updated_at\":\"2020-02-19T12:34:56.784Z\"}", 44 | "headers": { 45 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 46 | "Content-Type": "application/json", 47 | "Content-Length": "144", 48 | "Connection": "keep-alive", 49 | "Server": "nginx", 50 | "X-Auth0-RequestId": "<>", 51 | "cache-control": "private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform" 52 | }, 53 | "status_code": 200, 54 | "type": "ok" 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/auth0-token-doesnt-expire.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "client_id=clientidKFpqmR7aO1iVUOQR1LMKbZvk&client_secret=clientsecrethzqAhvWpJhAehfkzupWelgut-hRh7ZYOBOmaXsCRryhy7bqWzOCx&code=code_abc&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback", 5 | "headers": { 6 | "content-type": "application/x-www-form-urlencoded", 7 | "accept": "application/json", 8 | "authorization": "Basic Y2xpZW50aWRLRnBxbVI3YU8xaVZVT1FSMUxNS2Jadms6Y2xpZW50c2VjcmV0aHpxQWh2V3BKaEFlaGZrenVwV2VsZ3V0LWhSaDdaWU9CT21hWHNDUnJ5aHk3YnFXek9DeA==" 9 | }, 10 | "method": "post", 11 | "options": [], 12 | "request_body": "", 13 | "url": "https://example-app.auth0.com/oauth/token" 14 | }, 15 | "response": { 16 | "binary": false, 17 | "body": "{\"access_token\":\"eyJz93alolk4laUWw\",\"refresh_token\":\"GEbRxBNkitedjnXbL\",\"id_token\":\"eyJ0XAipop4faeEoQ\",\"token_type\":\"Bearer\"}", 18 | "headers": { 19 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 20 | "Content-Type": "application/json", 21 | "Content-Length": "144", 22 | "Connection": "keep-alive", 23 | "X-Auth0-RequestId": "<>" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | }, 29 | { 30 | "request": { 31 | "body": "", 32 | "headers": { 33 | "accept": "application/json", 34 | "authorization": "Bearer eyJz93alolk4laUWw" 35 | }, 36 | "method": "get", 37 | "options": [], 38 | "request_body": "", 39 | "url": "https://example-app.auth0.com/userinfo" 40 | }, 41 | "response": { 42 | "binary": false, 43 | "body": "{\"email\":\"testuser@example.com\",\"email_verified\":false,\"name\":\"testuser@example.com\",\"nickname\":\"testuser\",\"picture\":\"https://s.gravatar.com/avatar/7ec7606c46a14a7ef514d1f1f9038823?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Ftu.png\",\"sub\":\"auth0|lyy5v5utb6n9qfm4ihi3l7pv34po66\",\"updated_at\":\"2020-02-19T12:34:56.784Z\"}", 44 | "headers": { 45 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 46 | "Content-Type": "application/json", 47 | "Content-Length": "144", 48 | "Connection": "keep-alive", 49 | "Server": "nginx", 50 | "X-Auth0-RequestId": "<>", 51 | "cache-control": "private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform" 52 | }, 53 | "status_code": 200, 54 | "type": "ok" 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/auth0-userinfo-invalid-access-token.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "client_id=clientidKFpqmR7aO1iVUOQR1LMKbZvk&client_secret=clientsecrethzqAhvWpJhAehfkzupWelgut-hRh7ZYOBOmaXsCRryhy7bqWzOCx&code=code_abc&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback", 5 | "headers": { 6 | "content-type": "application/x-www-form-urlencoded", 7 | "accept": "application/json", 8 | "authorization": "Basic Y2xpZW50aWRLRnBxbVI3YU8xaVZVT1FSMUxNS2Jadms6Y2xpZW50c2VjcmV0aHpxQWh2V3BKaEFlaGZrenVwV2VsZ3V0LWhSaDdaWU9CT21hWHNDUnJ5aHk3YnFXek9DeA==" 9 | }, 10 | "method": "post", 11 | "options": [], 12 | "request_body": "", 13 | "url": "https://example-app.auth0.com/oauth/token" 14 | }, 15 | "response": { 16 | "binary": false, 17 | "body": "{\"access_token\":\"eyJz93alolk4laUWw\",\"refresh_token\":\"GEbRxBNkitedjnXbL\",\"id_token\":\"eyJ0XAipop4faeEoQ\",\"token_type\":\"Bearer\",\"expires_in\":86400}", 18 | "headers": { 19 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 20 | "Content-Type": "application/json", 21 | "Content-Length": "144", 22 | "Connection": "keep-alive", 23 | "X-Auth0-RequestId": "<>" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | }, 29 | { 30 | "request": { 31 | "body": "", 32 | "headers": { 33 | "accept": "application/json", 34 | "authorization": "Bearer eyJz93alolk4laUWw" 35 | }, 36 | "method": "get", 37 | "options": [], 38 | "request_body": "", 39 | "url": "https://example-app.auth0.com/userinfo" 40 | }, 41 | "response": { 42 | "binary": false, 43 | "body": "{\"error\":\"something_wrong\",\"error_description\":\"Something went wrong\"}", 44 | "headers": { 45 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 46 | "Content-Type": "application/json", 47 | "Content-Length": "70", 48 | "Connection": "keep-alive", 49 | "Server": "nginx", 50 | "X-Auth0-RequestId": "<>", 51 | "cache-control": "private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform" 52 | }, 53 | "status_code": 401, 54 | "type": "ok" 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /test/fixtures/vcr_cassettes/auth0-userinfo-with-errors-in-body.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "body": "client_id=clientidKFpqmR7aO1iVUOQR1LMKbZvk&client_secret=clientsecrethzqAhvWpJhAehfkzupWelgut-hRh7ZYOBOmaXsCRryhy7bqWzOCx&code=code_abc&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback", 5 | "headers": { 6 | "content-type": "application/x-www-form-urlencoded", 7 | "accept": "application/json", 8 | "authorization": "Basic Y2xpZW50aWRLRnBxbVI3YU8xaVZVT1FSMUxNS2Jadms6Y2xpZW50c2VjcmV0aHpxQWh2V3BKaEFlaGZrenVwV2VsZ3V0LWhSaDdaWU9CT21hWHNDUnJ5aHk3YnFXek9DeA==" 9 | }, 10 | "method": "post", 11 | "options": [], 12 | "request_body": "", 13 | "url": "https://example-app.auth0.com/oauth/token" 14 | }, 15 | "response": { 16 | "binary": false, 17 | "body": "{\"access_token\":\"eyJz93alolk4laUWw\",\"refresh_token\":\"GEbRxBNkitedjnXbL\",\"id_token\":\"eyJ0XAipop4faeEoQ\",\"token_type\":\"Bearer\",\"expires_in\":86400}", 18 | "headers": { 19 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 20 | "Content-Type": "application/json", 21 | "Content-Length": "144", 22 | "Connection": "keep-alive", 23 | "X-Auth0-RequestId": "<>" 24 | }, 25 | "status_code": 200, 26 | "type": "ok" 27 | } 28 | }, 29 | { 30 | "request": { 31 | "body": "", 32 | "headers": { 33 | "accept": "application/json", 34 | "authorization": "Bearer eyJz93alolk4laUWw" 35 | }, 36 | "method": "get", 37 | "options": [], 38 | "request_body": "", 39 | "url": "https://example-app.auth0.com/userinfo" 40 | }, 41 | "response": { 42 | "binary": false, 43 | "body": "{\"error\":\"something_wrong\",\"error_description\":\"Something went wrong\"}", 44 | "headers": { 45 | "Date": "Wed, 17 Jun 2020 23:39:36 GMT", 46 | "Content-Type": "application/json", 47 | "Content-Length": "70", 48 | "Connection": "keep-alive", 49 | "Server": "nginx", 50 | "X-Auth0-RequestId": "<>", 51 | "cache-control": "private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, no-transform" 52 | }, 53 | "status_code": 403, 54 | "type": "ok" 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /test/strategy/auth0/oauth_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Auth0.OAuthTest do 2 | use ExUnit.Case 3 | 4 | import Ueberauth.Strategy.Auth0.OAuth, only: [client: 0, client: 1] 5 | 6 | @test_domain "example-app.auth0.com" 7 | 8 | setup do 9 | {:ok, %{client: client()}} 10 | end 11 | 12 | test "creates correct client", %{client: client} do 13 | assert client.client_id == "clientidsomethingrandom" 14 | assert client.client_secret == "clientsecret-somethingsecret" 15 | assert client.redirect_uri == "" 16 | assert client.strategy == Ueberauth.Strategy.Auth0.OAuth 17 | assert client.authorize_url == "https://#{@test_domain}/authorize" 18 | assert client.token_url == "https://#{@test_domain}/oauth/token" 19 | assert client.site == "https://#{@test_domain}" 20 | end 21 | 22 | test "raises when there is no configuration" do 23 | assert_raise(RuntimeError, ~r/^Expected to find settings under.*/, fn -> 24 | client(otp_app: :unknown_auth0_otp_app) 25 | end) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/strategy/auth0_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Auth0Test do 2 | # Test resources: 3 | use ExUnit.Case, async: true 4 | use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney 5 | 6 | use Plug.Test 7 | 8 | # Custom data: 9 | import Ueberauth.Strategy.Auth0, only: [info: 1, extra: 1] 10 | alias Ueberauth.Auth.{Extra, Info} 11 | 12 | # Initializing utils: 13 | doctest Ueberauth.Strategy.Auth0 14 | 15 | @router SpecRouter.init([]) 16 | @test_email "janedoe@example.com" 17 | @session_options Plug.Session.init( 18 | store: Plug.Session.COOKIE, 19 | key: "_my_key", 20 | signing_salt: "CXlmrshG" 21 | ) 22 | 23 | # Setups: 24 | setup_all do 25 | # Creating token: 26 | token = %OAuth2.AccessToken{ 27 | access_token: "eyJz93alolk4laUWw", 28 | expires_at: 1_592_551_369, 29 | other_params: %{"id_token" => "eyJ0XAipop4faeEoQ"}, 30 | refresh_token: "GEbRxBNkitedjnXbL", 31 | token_type: "Bearer" 32 | } 33 | 34 | # Read the fixture with the user information: 35 | {:ok, json} = 36 | "test/fixtures/auth0.json" 37 | |> Path.expand() 38 | |> File.read() 39 | 40 | user_info = Jason.decode!(json) 41 | 42 | {:ok, 43 | %{ 44 | user_info: user_info, 45 | token: token 46 | }} 47 | end 48 | 49 | # Tests: 50 | describe "handle_request!" do 51 | test "simple oauth2 /authorize request" do 52 | conn = 53 | :get 54 | |> conn("/auth/auth0") 55 | |> SpecRouter.call(@router) 56 | 57 | assert conn.resp_body =~ ~s|You are being redirected.| 59 | assert conn.resp_body =~ ~s|href="https://example-app.auth0.com/authorize?| 60 | assert conn.resp_body =~ ~s|client_id=clientidsomethingrandom| 61 | 62 | assert conn.resp_body =~ 63 | ~s|redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback| 64 | 65 | assert conn.resp_body =~ ~s|response_type=code| 66 | assert conn.resp_body =~ ~s|scope=openid+profile+email| 67 | assert conn.resp_body =~ ~s|state=#{conn.private[:ueberauth_state_param]}| 68 | end 69 | 70 | test "advanced oauth2 /authorize request" do 71 | conn = 72 | :get 73 | |> conn( 74 | "/auth/auth0?scope=profile%20address%20phone&audience=https%3A%2F%2Fexample-app.auth0.com%2Fmfa%2F" <> 75 | "&connection=facebook&unknown_param=should_be_ignored" <> 76 | "&prompt=login&screen_hint=signup&login_hint=user%40example.com" <> 77 | "&organization=org_abc123&invitation=INVITE2022" 78 | ) 79 | |> SpecRouter.call(@router) 80 | 81 | assert conn.resp_body =~ ~s|You are being redirected.| 83 | assert conn.resp_body =~ ~s|href="https://example-app.auth0.com/authorize?| 84 | assert conn.resp_body =~ ~s|client_id=clientidsomethingrandom| 85 | assert conn.resp_body =~ ~s|connection=facebook| 86 | assert conn.resp_body =~ ~s|login_hint=user| 87 | assert conn.resp_body =~ ~s|screen_hint=signup| 88 | 89 | assert conn.resp_body =~ 90 | ~s|redirect_uri=http%3A%2F%2Fwww.example.com%2Fauth%2Fauth0%2Fcallback| 91 | 92 | assert conn.resp_body =~ ~s|response_type=code| 93 | assert conn.resp_body =~ ~s|scope=profile+address+phone| 94 | assert conn.resp_body =~ ~s|state=#{conn.private[:ueberauth_state_param]}| 95 | assert conn.resp_body =~ ~s|organization=org_abc123| 96 | assert conn.resp_body =~ ~s|invitation=INVITE2022| 97 | end 98 | end 99 | 100 | describe "handle_callback!" do 101 | test "nominal callback from auth0" do 102 | request_conn = 103 | :get 104 | |> conn("/auth/auth0", id: "foo") 105 | |> SpecRouter.call(@router) 106 | |> Plug.Conn.fetch_cookies() 107 | 108 | state = request_conn.private[:ueberauth_state_param] 109 | code = "some_code" 110 | 111 | use_cassette "auth0-ok-response", match_requests_on: [:query] do 112 | conn = 113 | :get 114 | |> conn("/auth/auth0/callback", 115 | id: "foo", 116 | code: code, 117 | state: state 118 | ) 119 | |> Map.put(:cookies, request_conn.cookies) 120 | |> Map.put(:req_cookies, request_conn.req_cookies) 121 | |> Plug.Session.call(@session_options) 122 | |> SpecRouter.call(@router) 123 | 124 | assert conn.resp_body == "auth0 callback" 125 | 126 | auth = conn.assigns.ueberauth_auth 127 | 128 | assert auth.provider == :auth0 129 | assert auth.strategy == Ueberauth.Strategy.Auth0 130 | assert auth.uid == "auth0|lyy5v5utb6n9qfm4ihi3l7pv34po66" 131 | assert conn.private.auth0_state == state 132 | 133 | ## Tokens have expiration time (see other test below) 134 | assert auth.credentials.expires == true 135 | assert is_integer(auth.credentials.expires_at) 136 | assert auth.credentials.scopes == ["openid", "profile", "email"] 137 | end 138 | end 139 | 140 | test "nominal callback from auth0 but without state: potential CSRF attack" do 141 | request_conn = 142 | :get 143 | |> conn("/auth/auth0", id: "foo") 144 | |> SpecRouter.call(@router) 145 | |> Plug.Conn.fetch_cookies() 146 | 147 | code = "some_code" 148 | 149 | use_cassette "auth0-ok-response", match_requests_on: [:query] do 150 | conn = 151 | :get 152 | |> conn("/auth/auth0/callback", 153 | id: "foo", 154 | code: code 155 | ) 156 | |> Map.put(:cookies, request_conn.cookies) 157 | |> Map.put(:req_cookies, request_conn.req_cookies) 158 | |> Plug.Session.call(@session_options) 159 | |> SpecRouter.call(@router) 160 | 161 | assert conn.resp_body == "auth0 callback" 162 | 163 | auth = conn.assigns.ueberauth_failure 164 | assert conn.private[:auth0_state] == nil 165 | 166 | csrf_attack = %Ueberauth.Failure.Error{ 167 | message: "Cross-Site Request Forgery attack", 168 | message_key: "csrf_attack" 169 | } 170 | 171 | assert auth.provider == :auth0 172 | assert auth.strategy == Ueberauth.Strategy.Auth0 173 | assert auth.errors == [csrf_attack] 174 | end 175 | end 176 | 177 | test "invalid callback from auth0 without code" do 178 | request_conn = 179 | :get 180 | |> conn("/auth/auth0", id: "foo") 181 | |> SpecRouter.call(@router) 182 | |> Plug.Conn.fetch_cookies() 183 | 184 | state = request_conn.private[:ueberauth_state_param] 185 | 186 | use_cassette "auth0-ok-response", match_requests_on: [:query] do 187 | conn = 188 | :get 189 | |> conn("/auth/auth0/callback", 190 | id: "foo", 191 | state: state 192 | ) 193 | |> Map.put(:cookies, request_conn.cookies) 194 | |> Map.put(:req_cookies, request_conn.req_cookies) 195 | |> Plug.Session.call(@session_options) 196 | |> SpecRouter.call(@router) 197 | 198 | assert conn.resp_body == "auth0 callback" 199 | 200 | auth = conn.assigns.ueberauth_failure 201 | 202 | missing_code_error = %Ueberauth.Failure.Error{ 203 | message: "No code received", 204 | message_key: "missing_code" 205 | } 206 | 207 | assert auth.provider == :auth0 208 | assert auth.strategy == Ueberauth.Strategy.Auth0 209 | assert auth.errors == [missing_code_error] 210 | end 211 | end 212 | 213 | test "invalid callback from auth0 with invalid code" do 214 | request_conn = 215 | :get 216 | |> conn("/auth/auth0", id: "foo") 217 | |> SpecRouter.call(@router) 218 | |> Plug.Conn.fetch_cookies() 219 | 220 | state = request_conn.private[:ueberauth_state_param] 221 | 222 | use_cassette "auth0-invalid-code", match_requests_on: [:query] do 223 | conn = 224 | :get 225 | |> conn("/auth/auth0/callback", id: "foo", code: "invalid_code", state: state) 226 | |> Map.put(:cookies, request_conn.cookies) 227 | |> Map.put(:req_cookies, request_conn.req_cookies) 228 | |> Plug.Session.call(@session_options) 229 | |> SpecRouter.call(@router) 230 | 231 | auth = conn.assigns.ueberauth_failure 232 | 233 | invalid_grant_error = %Ueberauth.Failure.Error{ 234 | message: "Invalid authorization code", 235 | message_key: "invalid_grant" 236 | } 237 | 238 | assert auth.provider == :auth0 239 | assert auth.strategy == Ueberauth.Strategy.Auth0 240 | assert auth.errors == [invalid_grant_error] 241 | end 242 | end 243 | 244 | test "invalid callback from auth0 with no token in response" do 245 | request_conn = 246 | :get 247 | |> conn("/auth/auth0", id: "foo") 248 | |> SpecRouter.call(@router) 249 | |> Plug.Conn.fetch_cookies() 250 | 251 | state = request_conn.private[:ueberauth_state_param] 252 | 253 | use_cassette "auth0-no-access-token", match_requests_on: [:query] do 254 | conn = 255 | :get 256 | |> conn("/auth/auth0/callback", 257 | id: "foo", 258 | code: "some_code", 259 | state: state 260 | ) 261 | |> Map.put(:cookies, request_conn.cookies) 262 | |> Map.put(:req_cookies, request_conn.req_cookies) 263 | |> Plug.Session.call(@session_options) 264 | |> SpecRouter.call(@router) 265 | 266 | assert conn.resp_body == "auth0 callback" 267 | 268 | auth = conn.assigns.ueberauth_failure 269 | 270 | missing_code_error = %Ueberauth.Failure.Error{ 271 | message: "Something went wrong", 272 | message_key: "something_wrong" 273 | } 274 | 275 | assert auth.provider == :auth0 276 | assert auth.strategy == Ueberauth.Strategy.Auth0 277 | assert auth.errors == [missing_code_error] 278 | end 279 | end 280 | 281 | test "callback from auth0 with no expiration time of tokens" do 282 | request_conn = 283 | :get 284 | |> conn("/auth/auth0", id: "foo") 285 | |> SpecRouter.call(@router) 286 | |> Plug.Conn.fetch_cookies() 287 | 288 | state = request_conn.private[:ueberauth_state_param] 289 | 290 | use_cassette "auth0-token-doesnt-expire", match_requests_on: [:query] do 291 | conn = 292 | :get 293 | |> conn("/auth/auth0/callback", 294 | id: "foo", 295 | code: "some_code", 296 | state: state 297 | ) 298 | |> Map.put(:cookies, request_conn.cookies) 299 | |> Map.put(:req_cookies, request_conn.req_cookies) 300 | |> Plug.Session.call(@session_options) 301 | |> SpecRouter.call(@router) 302 | 303 | assert conn.resp_body == "auth0 callback" 304 | 305 | auth = conn.assigns.ueberauth_auth 306 | 307 | # Same information as default token 308 | assert auth.provider == :auth0 309 | assert auth.strategy == Ueberauth.Strategy.Auth0 310 | assert auth.uid == "auth0|lyy5v5utb6n9qfm4ihi3l7pv34po66" 311 | assert conn.private.auth0_state == state 312 | 313 | ## Difference here 314 | assert auth.credentials.expires == false 315 | assert auth.credentials.expires_at == nil 316 | end 317 | end 318 | end 319 | 320 | test "/userinfo call with unauthorized access token" do 321 | request_conn = 322 | :get 323 | |> conn("/auth/auth0", id: "foo") 324 | |> SpecRouter.call(@router) 325 | |> Plug.Conn.fetch_cookies() 326 | 327 | state = request_conn.private[:ueberauth_state_param] 328 | code = "some_code" 329 | 330 | use_cassette "auth0-userinfo-invalid-access-token", match_requests_on: [:query] do 331 | conn = 332 | :get 333 | |> conn("/auth/auth0/callback", 334 | id: "foo", 335 | code: code, 336 | state: state 337 | ) 338 | |> Map.put(:cookies, request_conn.cookies) 339 | |> Map.put(:req_cookies, request_conn.req_cookies) 340 | |> Plug.Session.call(@session_options) 341 | |> SpecRouter.call(@router) 342 | 343 | assert conn.resp_body == "auth0 callback" 344 | 345 | auth = conn.assigns.ueberauth_failure 346 | 347 | token_unauthorized = %Ueberauth.Failure.Error{ 348 | message: "unauthorized_token", 349 | message_key: "OAuth2" 350 | } 351 | 352 | assert auth.provider == :auth0 353 | assert auth.strategy == Ueberauth.Strategy.Auth0 354 | assert auth.errors == [token_unauthorized] 355 | end 356 | end 357 | 358 | test "/userinfo call with body containing error details" do 359 | request_conn = 360 | :get 361 | |> conn("/auth/auth0", id: "foo") 362 | |> SpecRouter.call(@router) 363 | |> Plug.Conn.fetch_cookies() 364 | 365 | state = request_conn.private[:ueberauth_state_param] 366 | code = "some_code" 367 | 368 | use_cassette "auth0-userinfo-with-errors-in-body", match_requests_on: [:query] do 369 | conn = 370 | :get 371 | |> conn("/auth/auth0/callback", 372 | id: "foo", 373 | code: code, 374 | state: state 375 | ) 376 | |> Map.put(:cookies, request_conn.cookies) 377 | |> Map.put(:req_cookies, request_conn.req_cookies) 378 | |> Plug.Session.call(@session_options) 379 | |> SpecRouter.call(@router) 380 | 381 | assert conn.resp_body == "auth0 callback" 382 | 383 | auth = conn.assigns.ueberauth_failure 384 | 385 | some_error_in_body = %Ueberauth.Failure.Error{ 386 | message: %{"error" => "something_wrong", "error_description" => "Something went wrong"}, 387 | message_key: "OAuth2" 388 | } 389 | 390 | assert auth.provider == :auth0 391 | assert auth.strategy == Ueberauth.Strategy.Auth0 392 | assert auth.errors == [some_error_in_body] 393 | end 394 | end 395 | 396 | describe "info/1" do 397 | test "user information parsing", fixtures do 398 | user_info = fixtures.user_info 399 | token = fixtures.token 400 | 401 | conn = %Plug.Conn{ 402 | private: %{ 403 | auth0_user: user_info, 404 | auth0_token: token 405 | } 406 | } 407 | 408 | assert info(conn) == %Info{ 409 | birthday: "1972-03-31", 410 | description: nil, 411 | email: @test_email, 412 | first_name: "Jane", 413 | image: "http://example.com/janedoe/me.jpg", 414 | last_name: "Doe", 415 | location: nil, 416 | name: "Jane Josephine Doe", 417 | nickname: "JJ", 418 | phone: "+1 (111) 222-3434", 419 | urls: %{ 420 | profile: "http://example.com/janedoe", 421 | website: "http://example.com" 422 | } 423 | } 424 | end 425 | end 426 | 427 | describe "extra/1" do 428 | test "user extra information parsing", fixtures do 429 | user_info = fixtures.user_info 430 | token = fixtures.token 431 | 432 | conn = %Plug.Conn{ 433 | private: %{ 434 | auth0_user: user_info, 435 | auth0_token: token 436 | } 437 | } 438 | 439 | assert extra(conn) == %Extra{ 440 | raw_info: %{ 441 | token: %OAuth2.AccessToken{ 442 | access_token: "eyJz93alolk4laUWw", 443 | expires_at: 1_592_551_369, 444 | other_params: %{"id_token" => "eyJ0XAipop4faeEoQ"}, 445 | refresh_token: "GEbRxBNkitedjnXbL", 446 | token_type: "Bearer" 447 | }, 448 | user: %{ 449 | "address" => %{"country" => "us"}, 450 | "birthdate" => "1972-03-31", 451 | "email" => "janedoe@example.com", 452 | "email_verified" => true, 453 | "family_name" => "Doe", 454 | "gender" => "female", 455 | "given_name" => "Jane", 456 | "locale" => "en-US", 457 | "middle_name" => "Josephine", 458 | "name" => "Jane Josephine Doe", 459 | "nickname" => "JJ", 460 | "phone_number" => "+1 (111) 222-3434", 461 | "phone_number_verified" => false, 462 | "picture" => "http://example.com/janedoe/me.jpg", 463 | "preferred_username" => "j.doe", 464 | "profile" => "http://example.com/janedoe", 465 | "sub" => "auth0|lyy5v452u345tbn943qf", 466 | "updated_at" => "1556845729", 467 | "website" => "http://example.com", 468 | "zoneinfo" => "America/Los_Angeles" 469 | } 470 | } 471 | } 472 | end 473 | end 474 | end 475 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule SpecRouter do 2 | # Credit goes to: 3 | # https://github.com/he9qi/ueberauth_weibo/blob/master/test/test_helper.exs 4 | # and 5 | # https://github.com/ueberauth/ueberauth_vk/blob/master/test/test_helper.exs 6 | 7 | require Ueberauth 8 | use Plug.Router 9 | 10 | @session_options [ 11 | store: :cookie, 12 | key: "_my_key", 13 | signing_salt: "CXlmrshG" 14 | ] 15 | 16 | plug(Plug.Session, @session_options) 17 | 18 | plug(:fetch_query_params) 19 | 20 | plug(Ueberauth, base_path: "/auth") 21 | 22 | plug(:match) 23 | plug(:dispatch) 24 | 25 | get("/auth/auth0", do: send_resp(conn, 200, "auth0 request")) 26 | 27 | get("/auth/auth0/callback", do: send_resp(conn, 200, "auth0 callback")) 28 | end 29 | 30 | ExUnit.start() 31 | --------------------------------------------------------------------------------