Repository: dwyl/elixir-auth-google Branch: main Commit: 5d1ce87ebe3c Files: 16 Total size: 60.9 KB Directory structure: gitextract_jwgh7fao/ ├── .env_sample ├── .formatter.exs ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config/ │ ├── config.exs │ └── test.exs ├── create-google-app-guide.md ├── lib/ │ ├── elixir_auth_google.ex │ └── httpoison_mock.ex ├── mix.exs └── test/ ├── elixir_auth_google_test.exs └── test_helper.exs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .env_sample ================================================ export GOOGLE_CLIENT_ID=YourAppsClientId.apps.googleusercontent.com export GOOGLE_CLIENT_SECRET=SuperSecret # export GOOGLE_SCOPE=optional see README.md ================================================ FILE: .formatter.exs ================================================ # Used by "mix format" [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ] ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: mix directory: "/" schedule: interval: monthly time: "17:00" timezone: Europe/London ================================================ FILE: .github/workflows/ci.yml ================================================ name: Elixir CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: name: Build and test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Elixir uses: erlef/setup-beam@v1 with: elixir-version: '1.14.2' # Define the elixir version [required] otp-version: '25.1.2' # Define the OTP version [required] - name: Restore dependencies cache uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} restore-keys: ${{ runner.os }}-mix- - name: Install dependencies run: mix deps.get - name: Run Tests run: mix coveralls.json env: MIX_ENV: test - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 ================================================ FILE: .gitignore ================================================ /_build /cover /deps /doc /.fetch erl_crash.dump *.ez *.beam /config/*.secret.exs .elixir_ls .env elixir_auth_google.iml .idea ================================================ FILE: .travis.yml ================================================ language: elixir matrix: include: - elixir: 1.12.3 otp_release: 24.0.2 env: MIX_ENV=test script: - mix coveralls.json after_success: - bash <(curl -s https://codecov.io/bash) # send coverage report cache: directories: - _build - deps ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: README.md ================================================
# `elixir-auth-google` The _easiest_ way to add Google OAuth authentication to your Elixir Apps. ![sign-in-with-google-buttons](https://user-images.githubusercontent.com/194400/69637172-07a67900-1050-11ea-9e25-2b9e84a49d91.png) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/elixir-auth-google/ci.yml?label=build&style=flat-square&branch=main) [![codecov.io](https://img.shields.io/codecov/c/github/dwyl/elixir-auth-google/master.svg?style=flat-square)](http://codecov.io/github/dwyl/elixir-auth-google?branch=main) [![Hex.pm](https://img.shields.io/hexpm/v/elixir_auth_google?color=brightgreen&style=flat-square)](https://hex.pm/packages/elixir_auth_google) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/elixir-auth-google/issues) [![HitCount](http://hits.dwyl.com/dwyl/elixir-auth-google.svg)](http://hits.dwyl.com/dwyl/elixir-auth-google)
# _Why_? 🤷 We needed a **_much_ simpler** and **_extensively_ documented** way to add "_**Sign-in** with **Google**_" capability to our Elixir App(s).
# _What_? 💭 An Elixir package that seamlessly handles Google OAuth2 Authentication/Authorization in as few steps as possible.
Following best practices for security & privacy and avoiding complexity by having sensible defaults for all settings. > We built a lightweight solution that only does _one_ thing and is easy for complete beginners to understand/use.
There were already _several_ available options for adding Google Auth to apps on [hex.pm/packages?search=google](https://hex.pm/packages?search=google)
that all added _far_ too many implementation steps (complexity) and had incomplete docs (**`@doc false`**) and tests.
e.g: [github.com/googleapis/elixir-google-api](https://github.com/googleapis/elixir-google-api) which is a ["_generated_"](https://github.com/googleapis/elixir-google-api/blob/master/scripts/generate_client.sh) client and is considered "experimental".
We have drawn inspiration from several sources including code from other programming languages to build this package. This result is _much_ simpler than anything else and has both step-by-step instructions and a _complete working example_ App including how to encrypt tokens for secure storage to help you ship your app _fast_. # _Who_? 👥 This module is for people building apps using Elixir/Phoenix who want to ship the "Sign-in with Google" feature _faster_ and more maintainably. It's targetted at _complete_ beginners with no prior experience/knowledge of auth "schemes" or "strategies".
Just follow the detailed instructions and you'll be up-and running in 5 minutes. # _How_? ✅ You can add Google Authentication to your Elixir App using **`elixir_auth_google`**
in under **5 minutes** by following these **5 _easy_ steps**: ## 1. Add the hex package to `deps` 📦 Open your project's **`mix.exs`** file and locate the **`deps`** (dependencies) section.
Add a line for **`:elixir_auth_google`** in the **`deps`** list: ```elixir def deps do [ {:elixir_auth_google, "~> 1.6.9"} ] end ``` Once you have added the line to your **`mix.exs`**, remember to run the **`mix deps.get`** command in your terminal to _download_ the dependencies. ## 2. Create Google APIs Application OAuth2 Credentials 🆕 Create a Google Application if you don't already have one, generate the OAuth2 Credentials for the application and save the credentials as environment variables accessible by your app, or put them in your config file. > **Note**: There are a few steps for creating a set of Google APIs credentials, so if you don't already have a Google App, we created the following step-by-step guide to make it quick and _relatively_ painless: [create-google-app-guide.md](https://github.com/dwyl/elixir-auth-google/blob/master/create-google-app-guide.md)
Don't be intimidated by all the buzz-words; it's quite straightforward. And if you get stuck, ask for [help!](https://github.com/dwyl/elixir-auth-google/issues) ## 3. Setup CLIENT_ID and CLIENT_SECRET in your project You may either add those keys as environment variables or put them in the config: ``` export GOOGLE_CLIENT_ID=631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com export GOOGLE_CLIENT_SECRET=MHxv6-RGF5nheXnxh1b0LNDq ``` Or add the following in the config file: ```elixir config :elixir_auth_google, client_id: "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com", client_secret: "MHxv6-RGF5nheXnxh1b0LNDq" ``` > ⚠️ Don't worry, these keys aren't valid. They are just here for illustration purposes. ## 4. Create a `GoogleAuthController` in your Project 📝 Create a new file called [`lib/app_web/controllers/google_auth_controller.ex`](https://github.com/dwyl/elixir-auth-google-demo/blob/master/lib/app_web/controllers/google_auth_controller.ex) and add the following code: ```elixir defmodule AppWeb.GoogleAuthController do use AppWeb, :controller @doc """ `index/2` handles the callback from Google Auth API redirect. """ def index(conn, %{"code" => code}) do {:ok, token} = ElixirAuthGoogle.get_token(code, MyAppWeb.Endpoint.url()) {:ok, profile} = ElixirAuthGoogle.get_user_profile(token.access_token) conn |> put_view(AppWeb.PageView) |> render(:welcome, profile: profile) end end ``` This code does 3 things: + Create a one-time auth `token` based on the response `code` sent by Google after the person authenticates. + Request the person's profile data from Google based on the `access_token` + Render a `:welcome` view displaying some profile data to confirm that login with Google was successful. ## 5. Create the `/auth/google/callback` Endpoint 📍 Open your **`router.ex`** file and locate the section that looks like `scope "/", AppWeb do` Add the following line: ```elixir get "/auth/google/callback", GoogleAuthController, :index ``` Sample: [lib/app_web/router.ex#L20](https://github.com/dwyl/elixir-auth-google-demo/blob/4bb616dd134f498b84f079104c0f3345769517c4/lib/app_web/router.ex#L20) ### Different callback url? You can specify the env var ``` export GOOGLE_CALLBACK_PATH=/myauth/google_callback ``` or add it in the configuration Or add the following in the config file: ```elixir config :elixir_auth_google, # ... callback_path: "/myauth/google_callback" ``` ## 6. Add the "Login with Google" Button to your Template ✨ In order to display the "Sign-in with Google" button in the UI, we need to _generate_ the URL for the button in the relevant controller, and pass it to the template. Open the `lib/app_web/controllers/page_controller.ex` file and update the `index` function: From: ```elixir def index(conn, _params) do render(conn, "index.html") end ``` To: ```elixir def index(conn, _params) do oauth_google_url = ElixirAuthGoogle.generate_oauth_url(conn) render(conn, "index.html",[oauth_google_url: oauth_google_url]) end ``` You can add extra features the the generated url by passing the state as a string, or any other query as a map of key/value pairs. ```elixir oauth_google_url = ElixirAuthGoogle.generate_oauth_url(conn, state) ``` Will result in Google Sign In link with `&state=state` added. And something like: ```elixir oauth_google_url = ElixirAuthGoogle.generate_oauth_url(conn, %{lang: 'pt-BR'}) ``` Will return a url with `lang=pt-BR` included in the sign in request. #### _Alternatively_ pass the `url` of your `App` into `generate_oauth_url/1` We have noticed that on `fly.io` where the `Phoenix` App is proxied, passing the `conn` struct to `ElixirAuthGoogle.generate_oauth_url/2` is not effective. See [dwyl/elixir-auth-google/issues/94](https://github.com/dwyl/elixir-auth-google/issues/94) So we added an alternative way of invoking `generate_oauth_url/2` passing in the `url` of your `App`: ```elixir def index(conn, _params) do base_url = MyAppWeb.Endpoint.url() oauth_google_url = ElixirAuthGoogle.generate_oauth_url(base_url) render(conn, "index.html",[oauth_google_url: oauth_google_url]) end ``` This uses [Phoenix.Endpoint.url/0](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#c:url/0) which is available in any `Phoenix` App. Just remember to replace `MyAppWeb` with the name of your `App`. 😉
### 6.1 Update the `page/index.html.eex` Template Open the `/lib/app_web/templates/page/index.html.eex` file and type the following code: ```html

Welcome to Awesome App!

To get started, login to your Google Account:

Sign in with Google
``` # _Done_! 🚀 The home page of the app now has a big "Sign in with Google" button: ![sign-in-button](https://user-images.githubusercontent.com/194400/70202961-3c32c880-1713-11ea-9737-9121030ace06.png) When the person clicks the button, and authenticates with their Google Account, they will be returned to your App where you can display a "login success" message: ![welcome](https://user-images.githubusercontent.com/194400/70201692-494db880-170f-11ea-9776-0ffd1fdf5a72.png) ### _Optional_: Scopes Most of the time you will only want/need the person's email address and profile data when authenticating with your App. In the cases where you need more specific access to a Google service, you will need to specify the exact scopes. See: https://developers.google.com/identity/protocols/oauth2/scopes Once you know the scope(s) your App needs access to, simply define them using an environment variable, e.g: ``` GOOGLE_SCOPE="email contacts photoslibrary" ``` Those double-quotes (`"`) encapsulating the environment variable `String` are important. Without them your system will only assign the first word to the `GOOGLE_SCOPE`. e.g: `email` and ignore the remaining scopes you need. ***or*** you can set them as a config variable if you prefer: ``` config :elixir_auth_google, :google_scope: "email contacts photoslibrary" ``` With that configured, your App will gain access to the requested services once the person authenticates/authorizes.

## _Optimised_ SVG+CSS Button In **step 6.1** above, we suggest using an `` for the `Sign in with GitHub` button. But even though this image appears small **`389 × 93 px`** https://i.imgur.com/Kagbzkq.png it is "only" **`8kb`**: ![google-button-8kb](https://user-images.githubusercontent.com/194400/73607428-cd0c1000-45ad-11ea-8639-ffc3e9a0e0a2.png) We could spend some time in a graphics editor optimising the image, but we _know_ we can do better by using our `CSS` skills! 💡 > **Note**: This is the _official_ button provided by Google: [developers.google.com/identity/images/signin-assets.zip](developers.google.com/identity/images/signin-assets.zip)
So if there was any optimisation they could squeeze out of it, they probably would have done it before publishing the zip! The following code re-creates the `` using the GitHub logo **`SVG`** and `CSS` for layout/style: ```html
Sign in with Google
``` > We created this from scratch using the SVG of the Google logo and some basic CSS.
For the "making of" journey see: https://github.com/dwyl/elixir-auth-google/issues/25 The result looks _better_ than the `` button: ![img-vs-svg-8kb-1kb](https://user-images.githubusercontent.com/194400/73607841-54a84d80-45b3-11ea-9d0c-a81005a0bfde.png) It can be scaled to any screen size so it will _always_ look great!
Using http://bytesizematters.com we see that our SVG+CSS button is only **`1kb`**: ![bytesize-matters-google-button](https://user-images.githubusercontent.com/194400/73607378-4fe09b00-45ad-11ea-9ab1-3b383c1d4516.png) That is an **87.5%** bandwidth saving on the **`8kb`** of the [**`.png`** button](https://github.com/dwyl/elixir-auth-google/issues/25). And what's _more_ it reduces the number of HTTP requests which means the page loads _even_ faster. This is used in the Demo app: [`lib/app_web/templates/page/index.html.eex`](https://github.com/dwyl/elixir-auth-google-demo/blob/4fdbeada2f13f4dd27d2372a916764ec7aad24e7/lib/app_web/templates/page/index.html.eex#L5-L26) ### `i18n` The _biggest_ advantage of having an SVG+CSS button is that you can _translate_ the button text!
Since the text/copy of the button is now _just_ text in standard HTML, the user's web browser can _automatically_ translate it!
e.g: _French_ 🇬🇧 > 🇫🇷 ![google-login-french-translation](https://user-images.githubusercontent.com/194400/73607961-c03eea80-45b4-11ea-9840-5d5f02ff8a13.png) This is _much_ better UX for the **80%** of people in the world who do _not_ speak English _fluently_. The _single_ biggest engine for growth in startup companies is [_translating_](https://youtu.be/T9ikpoF2GH0?t=463) their user interface into more languages. Obviously don't focus on translations while you're building your MVP, but if it's no extra _work_ to use this SVG+CSS button and it means the person's web browser can _automatically_ localise your App! ### _Accessibility_ The `SVG+CSS` button is more accessible than the image. Even thought the `` had an `alt` attribute which is a lot better than nothing, the `SVG+CSS` button can be re-interpreted by a non-screen device and more easily transformed.

## _Even_ More Detail 💡 If you want to dive a bit deeper into _understanding_ how this package works, You can read and grok the code in under 10 minutes: [`/lib/elixir_auth_google.ex`](https://github.com/dwyl/elixir-auth-google/blob/master/lib/elixir_auth_google.ex) We created a _basic_ demo Phoenix App, to show you _exactly_ how you can implement the **`elixir_auth_google`** package: https://github.com/dwyl/elixir-auth-google-demo It's deployed to Heroku: https://elixir-auth-google-demo.herokuapp.com
(_no data is saved so you can play with it - and try to break it!_) And if you want/need a more complete real-world example including creating sessions and saving profile data to a database, take a look at our MVP: https://github.com/dwyl/app-mvp-phoenix

## Notes 📝 + Official Docs for Google Identity Platform: https://developers.google.com/identity/choose-auth + Web specific sample code (JS): https://developers.google.com/identity/sign-in/web + Google Sign-In for server-side apps: https://developers.google.com/identity/sign-in/web/server-side-flow + Using OAuth 2.0 for Web Server Applications: https://developers.google.com/identity/protocols/OAuth2WebServer + Google Auth Branding Guidelines: https://developers.google.com/identity/branding-guidelines
Only two colors are permitted for the button: **white** `#FFFFFF` and **blue** `#4285F4` ![two-colors-of-google-auth-button](https://user-images.githubusercontent.com/194400/69634312-d9be3600-1049-11ea-9354-cdaa53f5c42b.png) ### Fun Facts 📈📊 Unlike other "social media" companies, Google/Alphabet does not report it's _Monthly_ Active Users (MAUs) or _Daily_ Active Users (DAUs) however they do release stats in drips in their Google IO or YouTube events. The following is a quick list of facts that make adding Google Auth to your App a compelling business case: + As of May 2019, there are over [2.5 Billion](https://www.theverge.com/2019/5/7/18528297/google-io-2019-android-devices-play-store-total-number-statistic-keynote) _active_ Android devices; [87%](https://www.idc.com/promo/smartphone-market-share/os) global market share. All these people have Google Accounts in order to use Google services. + YouTube has [2 billion](https://www.businessofapps.com/data/youtube-statistics/) monthly active YouTube users (_signed in with a Google Account_). + Gmail has [1.5 Billion](https://www.thenewsminute.com/article/googles-gmail-turns-15-now-has-over-15-billion-monthly-active-users-99275) monthly active users a [27% share](https://seotribunal.com/blog/google-stats-and-facts) of the global email client market. + [65%](https://techjury.net/stats-about/gmail-statistics) of Small and Medium sized businesses use Google Apps for business. + [90%+](https://techjury.net/stats-about/gmail-statistics) of startups use Gmail. This is a good _proxy_ for "early adopters". + [68%](https://eu.azcentral.com/story/opinion/op-ed/joannaallhands/2017/10/09/google-classroom-changing-teachers-students-education/708246001/) of schools in the US use Google Classroom and related G-suite products.
So the _next_ generation of internet/app users have Google accounts. + Google has [90.46%](https://seotribunal.com/blog/google-stats-and-facts/) of the search engine market share worldwide. 95.4% on Mobile. Of the 4.5 billion internet users (58% of the world population), around 3.2 billion (72%) have a Google account. 90%+ of tech "early adopters" use Google Apps which means that adding Google OAuth Sign-in is the _logical_ choice for _most_ Apps. ### Privacy Concerns? 🔐 A _common misconception_ is that adding Google Auth Sign-in sends a user's application data to Google. This is **`false`** and App developers have 100% control over what data is sent to (stored by) Google. An App can use Google Auth to _authenticate_ a person (_identify them and get read-only access to their personal details like **first name** and **email address**_) without sending any data to Google. Yes, it will mean that Google "knows" that the person is _using_ your App, but it will not give Google any insight into _how_ they are using it or what types of data they are storing in the App. Privacy is maintained. So if you use the @dwyl app to plan your wedding or next holiday, Google will not have _any_ of that data and will not serve any annoying ads based on your project/plans. ================================================ FILE: config/config.exs ================================================ import Config if Mix.env() == :test do import_config "test.exs" end ================================================ FILE: config/test.exs ================================================ import Config config :elixir_auth_google, client_id: "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com", client_secret: "MHxv6-RGF5nheXnxh1b0LNDq", httpoison_mock: true System.put_env( "GOOGLE_CLIENT_ID", "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com" ) ================================================ FILE: create-google-app-guide.md ================================================ # Creating a Google Application for OAuth2 Authentication This is a step-by-step guide for creating a Google App from scratch so that you can obtain the API keys to add Google OAuth2 Authentication to your Elixir App and save the credentials to environment variables.
Our guide follows the _official_ docs: https://developers.google.com/identity/sign-in/web/server-side-flow
We've added detail and screenshots to the steps because some people have found the official Google API docs confusing.
_This_ guide is checked periodically by the @dwyl team/community, but Google are known to occasionally change their UI/Workflow, so if anything has changed, or there are extra/fewer steps, [please let us know!](https://github.com/dwyl/elixir-auth-google/issues) ## 1. Create a New Project In your preferred web browser, visit: https://console.developers.google.com and ensure you are authenticated with your Google Account so you can see your "API & Services Dashboard": elixir-auth-google-create-new-app If you don't already have a Google APIs project for your Elixir App, click the **CREATE** button on the dashboard. ## 2. Define the Details for your New Project (App) Enter the details for your App's **Project name** and where appropriate input any additional/relevant info: elixir-auth-google-app-details Click the **CREATE** button to create your project. ## 3. OAuth Consent Screen After creating the New Project, the UI will return to the APIs dashboard and the name of your app will appear in the top menu. Click the **OAuth Consent Screen** from the left side menu: elixir-auth-google-consent-screen Make the Application **`Public`** (_the default option_) and input the same name as you used for your application in step 1. Upload an image if you have one (_e.g: the icon/logo for your app_): OAuth-consent-screen-1of2 Leave the "**Scopes for Google APIs**" set to the default **email**, **profile** and **openid**. No other data is required at this point, so skip the rest. Scroll down to the bottom and click "**Save**": OAuth-consent-screen-2of2 This will take you to the API Credentials page. ## 4. Create Credentials Click the **Create Credentials** button: Screenshot 2019-11-26 at 23 24 28 That will popup a menu from which you will select **OAuth Client ID**. You will see a form that allows you to specify the details of your App for the credentials. Screenshot 2019-11-27 at 02 13 55 + Application Type: Web application + Name: Elixir Auth Server + Authorized JavaScript origins: http://localhost:4000 (_the default for a Phoenix app on your local dev machine. you can add your "production" URL later._) + Authorized redirect URIs: http://localhost:4000/auth/google/callback (_the endpoint to redirect to once authentication is successful. again, add your production URL once you have auth working on `localhost`_) > Ensure you hit the enter key after pasting/typing the URIs to ensure they are saved. A common stumbling block is that URIs aren't saved. See: https://stackoverflow.com/questions/24363041/redirect-uri-in-google-cloud-console-doesnt-save Once you have input the relevant data click the **Create** button. > This form/step can be confusing at first, but essentially you can have multiple credentials for the same project, e.g: if you had a Native Android App you would create a new set of credentials to ensure a separation of concerns between server and client implementations. For now just create the server (Elixir) credentials. ## 5. Download the OAuth Client Credentials After you click the **Create** button in the **Create OAuth client ID** form (_step 4 above_), you will be shown your OAuth client Credentials: elixir-auth-google-oauth-client-credentials Download the credentials, e.g: + Client ID: 631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com + Client Secret: MHxv6-RGF5nheXnxh1b0LNDq > ⚠️ Don't worry, these keys aren't valid. We deleted them immediately after capturing the screenshot to avoid any security issues. Obviously treat your credentials like you would the username+password for your bank account; never share a **real** Client ID or secret on GitHub or any other public/insecure forum! You can also download the OAuth credentials as a json file: elixir-auth-google-json Example: ```json { "web": { "client_id": "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com", "project_id": "elixir-auth-demo", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "MHxv6-RGF5nheXnxh1b0LNDq", "redirect_uris": [ "http://localhost:4000/auth/google/callback" ], "javascript_origins": [ "http://localhost:4000" ] } } ``` > Again, for security reasons, these credentials were invalidated _immediately_ after downloading.
But this is what the file looks like. Return to step 3 of the [README.md](https://github.com/dwyl/elixir-auth-google/blob/master/README.md)
# Note When you ship your app to your Production environment, you will need to re-visit steps 3 & 4 to update your app settings URL & callback to reflect the URl where you are deploying your app e.g: ![add-heroku-app](https://user-images.githubusercontent.com/194400/70204921-32f92a00-171a-11ea-83b2-34e5eeea777b.png) In our case https://elixir-auth-google-demo.herokuapp.com and https://elixir-auth-google-demo.herokuapp.com/auth/google/callback ================================================ FILE: lib/elixir_auth_google.ex ================================================ defmodule ElixirAuthGoogle do @moduledoc """ Minimalist Google OAuth Authentication for Elixir Apps. Extensively tested, documented, maintained and in active use in production. """ @google_auth_url "https://accounts.google.com/o/oauth2/v2/auth?response_type=code" @google_token_url "https://oauth2.googleapis.com/token" @google_user_profile "https://www.googleapis.com/oauth2/v3/userinfo" @default_scope "profile email" @default_callback_path "/auth/google/callback" @httpoison (Application.compile_env(:elixir_auth_google, :httpoison_mock) && ElixirAuthGoogle.HTTPoisonMock) || HTTPoison @type conn :: map @type url :: String.t() @doc """ `inject_poison/0` injects a TestDouble of HTTPoison in Test so that we don't have duplicate mock in consuming apps. see: github.com/dwyl/elixir-auth-google/issues/35 """ def inject_poison, do: @httpoison @doc """ `get_baseurl_from_conn/1` derives the base URL from the conn struct """ @spec get_baseurl_from_conn(conn) :: String.t() def get_baseurl_from_conn(%{host: h, port: p, scheme: s}) when p != 80 do "#{Atom.to_string(s)}://#{h}:#{p}" end def get_baseurl_from_conn(%{host: h, scheme: s}) do "#{Atom.to_string(s)}://#{h}" end def get_baseurl_from_conn(%{host: h} = conn) do scheme = case h do "localhost" -> :http _ -> :https end get_baseurl_from_conn(Map.put(conn, :scheme, scheme)) end @doc """ `generate_redirect_uri/1` generates the Google redirect uri based on `conn` or the `url`. If the `App.Endpoint.url()` e.g: auth.dwyl.com or https://gcal.fly.dev is passed into `generate_redirect_uri/1`, return that `url` with the callback appended to it. See: github.com/dwyl/elixir-auth-google/issues/94 """ @spec generate_redirect_uri(url) :: String.t() def generate_redirect_uri(url) when is_binary(url) do scheme = cond do # url already contains scheme return empty String.contains?(url, "https") -> "" # url contains ":" is localhost:4000 no need for scheme String.contains?(url, ":") -> "" # Default to https if scheme not set e.g: app.fly.dev -> https://app.fly.fev true -> "https://" end "#{scheme}#{url}" <> get_app_callback_url() end @spec generate_redirect_uri(conn) :: String.t() def generate_redirect_uri(conn) do get_baseurl_from_conn(conn) <> get_app_callback_url() end @doc """ `generate_oauth_url/1` creates the Google OAuth2 URL with client_id, scope and redirect_uri which is the URL Google will redirect to when auth is successful. This is the URL you need to use for your "Login with Google" button. See step 5 of the instructions. """ @spec generate_oauth_url(String.t()) :: String.t() def generate_oauth_url(url) when is_binary(url) do query = [ client_id: google_client_id(), redirect_uri: generate_redirect_uri(url), scope: google_scope() ] params = URI.encode_query(query, :rfc3986) "#{@google_auth_url}&#{params}" end @spec generate_oauth_url(conn) :: String.t() def generate_oauth_url(conn) when is_map(conn) do query = [ client_id: google_client_id(), redirect_uri: generate_redirect_uri(conn), scope: google_scope() ] params = URI.encode_query(query, :rfc3986) "#{@google_auth_url}&#{params}" end @doc """ Same as `generate_oauth_url/1` with `state` query parameter, or a `map` of key/pair values to be included in the urls query string. """ @spec generate_oauth_url(conn, String.t() | map) :: String.t() def generate_oauth_url(conn, state) when is_binary(state) do params = URI.encode_query(%{state: state}, :rfc3986) generate_oauth_url(conn) <> "&#{params}" end def generate_oauth_url(conn, query) when is_map(query) do query = URI.encode_query(query, :rfc3986) generate_oauth_url(conn) <> "&#{query}" end @doc """ `get_token/2` encodes the secret keys and authorization code returned by Google and issues an HTTP request to get a person's profile data. **TODO**: we still need to handle the various failure conditions >> issues/16 """ @spec get_token(String.t(), conn) :: {:ok, map} | {:error, any} def get_token(code, conn) when is_map(conn) do redirect_uri = generate_redirect_uri(conn) inject_poison().post(@google_token_url, req_body(code, redirect_uri)) |> parse_body_response() end @spec get_token(String.t(), url) :: {:ok, map} | {:error, any} def get_token(code, url) when is_binary(url) do redirect_uri = generate_redirect_uri(url) inject_poison().post(@google_token_url, req_body(code, redirect_uri)) |> parse_body_response() end defp req_body(code, redirect_uri) do Jason.encode!(%{ client_id: google_client_id(), client_secret: google_client_secret(), redirect_uri: redirect_uri, grant_type: "authorization_code", code: code }) end @doc """ `get_user_profile/1` requests the Google User's userinfo profile data providing the access_token received in the `get_token/1` above. invokes `parse_body_response/1` to decode the JSON data. **TODO**: we still need to handle the various failure conditions >> issues/16 At this point the types of errors we expect are HTTP 40x/50x responses. """ @spec get_user_profile(String.t()) :: {:ok, map} | {:error, any} def get_user_profile(token) do params = URI.encode_query(%{access_token: token}, :rfc3986) "#{@google_user_profile}?#{params}" |> inject_poison().get() |> parse_body_response() end @doc """ `parse_body_response/1` parses the response returned by Google so your app can use the resulting JSON. """ @spec parse_body_response({atom, String.t()} | {:error, any}) :: {:ok, map} | {:error, any} def parse_body_response({:error, err}), do: {:error, err} def parse_body_response({:ok, response}) do body = Map.get(response, :body) # make keys of map atoms for easier access in templates if body == nil do {:error, :no_body} else {:ok, str_key_map} = Jason.decode(body) atom_key_map = for {key, val} <- str_key_map, into: %{}, do: {String.to_atom(key), val} {:ok, atom_key_map} end # https://stackoverflow.com/questions/31990134 end def google_client_id do System.get_env("GOOGLE_CLIENT_ID") || Application.get_env(:elixir_auth_google, :client_id) end defp google_client_secret do System.get_env("GOOGLE_CLIENT_SECRET") || Application.get_env(:elixir_auth_google, :client_secret) end defp google_scope do System.get_env("GOOGLE_SCOPE") || Application.get_env(:elixir_auth_google, :google_scope) || @default_scope end defp get_app_callback_url do System.get_env("GOOGLE_CALLBACK_PATH") || Application.get_env(:elixir_auth_google, :callback_path) || @default_callback_path end end ================================================ FILE: lib/httpoison_mock.ex ================================================ defmodule ElixirAuthGoogle.HTTPoisonMock do @moduledoc """ This is a TestDouble for HTTPoison which returns a predictable response. Please see: https://github.com/dwyl/elixir-auth-google/issues/35 """ @doc """ get/1 passing in the wrong_token is used to test failure in the auth process. Obviously, don't invoke it from your App unless you want people to see fails. """ def get("https://www.googleapis.com/oauth2/v3/userinfo?access_token=wrong_token") do {:error, :bad_request} end # get/1 using a dummy _url to test body decoding. def get(_url) do {:ok, %{ body: Jason.encode!(%{ email: "nelson@gmail.com", email_verified: true, family_name: "Correia", given_name: "Nelson", locale: "en", name: "Nelson Correia", picture: "https://lh3.googleusercontent.com/a-/AAuE7mApnYb260YC1JY7a", sub: "940732358705212133793" }) }} end @doc """ post/2 passing in dummy _url & _body to test return of access_token. """ def post(_url, _body) do {:ok, %{body: Jason.encode!(%{access_token: "token1"})}} end end ================================================ FILE: mix.exs ================================================ defmodule ElixirAuthGoogle.MixProject do use Mix.Project @description "Minimalist Google OAuth Authentication for Elixir Apps" @version "1.6.11" def project do [ app: :elixir_auth_google, version: @version, elixir: ">= 1.11.0", start_permanent: Mix.env() == :prod, deps: deps(), description: @description, package: package(), aliases: aliases(), # coverage test_coverage: [tool: ExCoveralls] ] end def cli do [ preferred_envs: [ c: :test, coveralls: :test, "coveralls.json": :test, "coveralls.html": :test, t: :test ] ] end # Run "mix help compile.app" to learn about applications. def application do [ extra_applications: [:logger] ] end # Run "mix help deps" to learn about dependencies. defp deps do [ {:credo, "~> 1.5", only: [:dev, :test], runtime: false}, {:httpoison, "~> 2.3"}, {:jason, "~> 1.2"}, # Track test coverage: github.com/parroty/excoveralls {:excoveralls, "~> 0.18.0", only: [:test, :dev]}, # Mock stuffs in test: github.com/jjh42/mock {:mock, "~> 0.3.0", only: :test}, # documentation {:ex_doc, "~> 0.40.0", only: :dev} ] end defp package do [ maintainers: ["dwyl"], licenses: ["GPL-2.0-or-later"], links: %{github: "https://github.com/dwyl/elixir-auth-google"}, files: ~w(lib LICENSE mix.exs README.md .formatter.exs) ] end defp aliases do [ t: ["test"], c: ["coveralls.html"] ] end end ================================================ FILE: test/elixir_auth_google_test.exs ================================================ defmodule ElixirAuthGoogleTest do use ExUnit.Case, async: true doctest ElixirAuthGoogle import Mock test "get_baseurl_from_conn(conn) detects the URL based on conn.host HTTP" do conn = %{ host: "localhost", port: 4000, scheme: :http } assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "http://localhost:4000" end test "get_baseurl_from_conn(conn) detects the URL based on conn.host HTTPS" do conn = %{ host: "localhost", port: 4000, scheme: :https } assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "https://localhost:4000" end test "get_baseurl_from_conn(conn) detects the URL for production HTTPS" do conn = %{ host: "dwyl.com", port: 80, scheme: :https } assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "https://dwyl.com" end test "get_baseurl_from_conn(conn) detects the URL for production HTTP" do conn = %{ host: "dwyl.com", port: 80, scheme: :http } assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "http://dwyl.com" end test "get_baseurl_from_conn(conn) detects the URL for production HTTPS, non-standard port" do conn = %{ host: "dwyl.com", port: 8080, scheme: :https } assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "https://dwyl.com:8080" end test "get Google login url" do conn = %{ host: "localhost", port: 4000 } assert ElixirAuthGoogle.generate_oauth_url(conn) =~ "https://accounts.google.com/o/oauth2/v2/auth?response_type=code" end test "get Google login url (config redirect uri)" do conn = %{ host: "localhost", port: 4000 } url = ElixirAuthGoogle.generate_oauth_url(conn) assert url =~ "https://accounts.google.com/o/oauth2/v2/auth?response_type=code" assert url =~ "http%3A%2F%2Flocalhost%3A4000" end test "get Google login url with state" do conn = %{ host: "localhost", port: 4000 } url = ElixirAuthGoogle.generate_oauth_url(conn, "state1") id = System.get_env("GOOGLE_CLIENT_ID") id_from_config = Application.get_env(:elixir_auth_google, :client_id) assert id == id_from_config expected = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=" <> id <> "&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email&state=state1" assert url == expected end test "get Google login with language parameters" do conn = %{ host: "localhost", port: 4000 } url = ElixirAuthGoogle.generate_oauth_url(conn, %{hl: "es-MX"}) id = System.get_env("GOOGLE_CLIENT_ID") id_from_config = Application.get_env(:elixir_auth_google, :client_id) assert id == id_from_config expected = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=" <> id <> "&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email&hl=es-MX" assert url == expected end test "get Google token" do conn = %{ host: "localhost", port: 4000 } {:ok, res} = ElixirAuthGoogle.get_token("ok_code", conn) assert res == %{access_token: "token1"} end test "get Google token with url as second param #94" do {:ok, res} = ElixirAuthGoogle.get_token("ok_code", "gcal.fly.dev") assert res == %{access_token: "token1"} end test "get Google token (config redirect uri)" do conn = %{ host: "localhost", port: 4000 } {:ok, res} = ElixirAuthGoogle.get_token("ok_code", conn) assert res == %{access_token: "token1"} end test "get_user_profile/1" do res = %{ email: "nelson@gmail.com", email_verified: true, family_name: "Correia", given_name: "Nelson", locale: "en", name: "Nelson Correia", picture: "https://lh3.googleusercontent.com/a-/AAuE7mApnYb260YC1JY7a", sub: "940732358705212133793" } assert ElixirAuthGoogle.get_user_profile("123") == {:ok, res} end test "return error with incorrect token" do assert ElixirAuthGoogle.get_user_profile("wrong_token") == {:error, :bad_request} end test "generate_redirect_uri(conn) generate correct callback url" do conn = %{ host: "foobar.com", port: 80 } assert ElixirAuthGoogle.generate_redirect_uri(conn) == "https://foobar.com/auth/google/callback" end test "generate_oauth_url(url) passing in App.Endpoint.url() #94" do url = "gcal.fly.dev" client_id = ElixirAuthGoogle.google_client_id() https = "https%3A%2F%2F#{url}" auth_url = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=#{client_id}&redirect_uri=#{https}%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email" assert ElixirAuthGoogle.generate_oauth_url(url) =~ auth_url end test "generate_oauth_url(url) with scheme e.g. https://gcal.fly.dev #94" do no_scheme = "gcal.fly.dev" url = "https://#{no_scheme}" client_id = ElixirAuthGoogle.google_client_id() https = "https%3A%2F%2F#{no_scheme}" auth_url = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=#{client_id}&redirect_uri=#{https}%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email" assert ElixirAuthGoogle.generate_oauth_url(url) =~ auth_url end test "generate_redirect_uri(conn) generate correct callback url with custom url path from application environment variable" do conn = %{ host: "foobar.com", port: 80 } mock_get_env = fn :elixir_auth_google, :callback_path -> "/special/callback" end with_mock Application, get_env: mock_get_env do assert ElixirAuthGoogle.generate_redirect_uri(conn) == "https://foobar.com/special/callback" end end test "generate_redirect_uri(conn) generate correct callback url with custom url path from system environment variable" do conn = %{ host: "foobar.com", port: 80 } mock_get_env = fn "GOOGLE_CALLBACK_PATH" -> "/very/special/callback" end with_mock System, get_env: mock_get_env do assert ElixirAuthGoogle.generate_redirect_uri(conn) == "https://foobar.com/very/special/callback" end end end ================================================ FILE: test/test_helper.exs ================================================ ExUnit.start()