├── .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 ├── mix.lock └── test ├── elixir_auth_google_test.exs └── test_helper.exs /.env_sample: -------------------------------------------------------------------------------- 1 | export GOOGLE_CLIENT_ID=YourAppsClientId.apps.googleusercontent.com 2 | export GOOGLE_CLIENT_SECRET=SuperSecret 3 | # export GOOGLE_SCOPE=optional see README.md 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "17:00" 8 | timezone: Europe/London 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Elixir 16 | uses: erlef/setup-beam@v1 17 | with: 18 | elixir-version: '1.14.2' # Define the elixir version [required] 19 | otp-version: '25.1.2' # Define the OTP version [required] 20 | - name: Restore dependencies cache 21 | uses: actions/cache@v4 22 | with: 23 | path: deps 24 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 25 | restore-keys: ${{ runner.os }}-mix- 26 | - name: Install dependencies 27 | run: mix deps.get 28 | - name: Run Tests 29 | run: mix coveralls.json 30 | env: 31 | MIX_ENV: test 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v1 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | *.beam 9 | /config/*.secret.exs 10 | .elixir_ls 11 | .env 12 | elixir_auth_google.iml 13 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | matrix: 3 | include: 4 | - elixir: 1.12.3 5 | otp_release: 24.0.2 6 | env: 7 | MIX_ENV=test 8 | script: 9 | - mix coveralls.json 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) # send coverage report 12 | cache: 13 | directories: 14 | - _build 15 | - deps 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # `elixir-auth-google` 4 | 5 | The _easiest_ way to add Google OAuth authentication to your Elixir Apps. 6 | 7 | ![sign-in-with-google-buttons](https://user-images.githubusercontent.com/194400/69637172-07a67900-1050-11ea-9e25-2b9e84a49d91.png) 8 | 9 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/elixir-auth-google/ci.yml?label=build&style=flat-square&branch=main) 10 | [![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) 11 | [![Hex.pm](https://img.shields.io/hexpm/v/elixir_auth_google?color=brightgreen&style=flat-square)](https://hex.pm/packages/elixir_auth_google) 12 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/elixir-auth-google/issues) 13 | [![HitCount](http://hits.dwyl.com/dwyl/elixir-auth-google.svg)](http://hits.dwyl.com/dwyl/elixir-auth-google) 14 | 15 | 16 |
17 | 18 | # _Why_? 🤷 19 | 20 | We needed a **_much_ simpler** 21 | and **_extensively_ documented** way 22 | to add "_**Sign-in** with **Google**_" 23 | capability to our Elixir App(s).
24 | 25 | # _What_? 💭 26 | 27 | An Elixir package that seamlessly handles 28 | Google OAuth2 Authentication/Authorization 29 | in as few steps as possible.
30 | Following best practices for security & privacy 31 | and avoiding complexity 32 | by having sensible defaults for all settings. 33 | 34 | 35 | > We built a lightweight solution 36 | that only does _one_ thing 37 | and is easy for complete beginners to understand/use.
38 | There were already _several_ available options 39 | for adding Google Auth to apps on 40 | [hex.pm/packages?search=google](https://hex.pm/packages?search=google)
41 | that all added _far_ too many implementation steps (complexity) 42 | and had incomplete docs (**`@doc false`**) and tests.
43 | e.g: 44 | [github.com/googleapis/elixir-google-api](https://github.com/googleapis/elixir-google-api) 45 | which is a 46 | ["_generated_"](https://github.com/googleapis/elixir-google-api/blob/master/scripts/generate_client.sh) 47 | client and is considered "experimental".
48 | We have drawn inspiration from several sources 49 | including code from other programming languages to build this package. 50 | This result is _much_ simpler 51 | than anything else 52 | and has both step-by-step instructions 53 | and a _complete working example_ App 54 | including how to encrypt tokens for secure storage 55 | to help you ship your app _fast_. 56 | 57 | 58 | # _Who_? 👥 59 | 60 | This module is for people building apps using Elixir/Phoenix 61 | who want to ship the "Sign-in with Google" feature _faster_ 62 | and more maintainably. 63 | 64 | It's targetted at _complete_ beginners 65 | with no prior experience/knowledge 66 | of auth "schemes" or "strategies".
67 | Just follow the detailed instructions 68 | and you'll be up-and running in 5 minutes. 69 | 70 | 71 | # _How_? ✅ 72 | 73 | You can add Google Authentication to your Elixir App 74 | using **`elixir_auth_google`**
75 | in under **5 minutes** 76 | by following these **5 _easy_ steps**: 77 | 78 | ## 1. Add the hex package to `deps` 📦 79 | 80 | Open your project's **`mix.exs`** file 81 | and locate the **`deps`** (dependencies) section.
82 | Add a line for **`:elixir_auth_google`** in the **`deps`** list: 83 | 84 | ```elixir 85 | def deps do 86 | [ 87 | {:elixir_auth_google, "~> 1.6.9"} 88 | ] 89 | end 90 | ``` 91 | 92 | Once you have added the line to your **`mix.exs`**, 93 | remember to run the **`mix deps.get`** command 94 | in your terminal 95 | to _download_ the dependencies. 96 | 97 | 98 | ## 2. Create Google APIs Application OAuth2 Credentials 🆕 99 | 100 | Create a Google Application if you don't already have one, 101 | generate the OAuth2 Credentials for the application 102 | and save the credentials as environment variables 103 | accessible by your app, or put them in your config file. 104 | 105 | > **Note**: There are a few steps for creating a set of Google APIs credentials, 106 | so if you don't already have a Google App, 107 | we created the following step-by-step guide 108 | to make it quick and _relatively_ painless: 109 | [create-google-app-guide.md](https://github.com/dwyl/elixir-auth-google/blob/master/create-google-app-guide.md)
110 | Don't be intimidated by all the buzz-words; 111 | it's quite straightforward. 112 | And if you get stuck, ask for 113 | [help!](https://github.com/dwyl/elixir-auth-google/issues) 114 | 115 | 116 | ## 3. Setup CLIENT_ID and CLIENT_SECRET in your project 117 | 118 | You may either add those keys as environment variables or put them in the config: 119 | 120 | ``` 121 | export GOOGLE_CLIENT_ID=631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com 122 | export GOOGLE_CLIENT_SECRET=MHxv6-RGF5nheXnxh1b0LNDq 123 | ``` 124 | Or add the following in the config file: 125 | 126 | ```elixir 127 | config :elixir_auth_google, 128 | client_id: "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com", 129 | client_secret: "MHxv6-RGF5nheXnxh1b0LNDq" 130 | 131 | ``` 132 | > ⚠️ Don't worry, these keys aren't valid. 133 | They are just here for illustration purposes. 134 | 135 | 136 | ## 4. Create a `GoogleAuthController` in your Project 📝 137 | 138 | Create a new file called 139 | [`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) 140 | and add the following code: 141 | 142 | ```elixir 143 | defmodule AppWeb.GoogleAuthController do 144 | use AppWeb, :controller 145 | 146 | @doc """ 147 | `index/2` handles the callback from Google Auth API redirect. 148 | """ 149 | def index(conn, %{"code" => code}) do 150 | {:ok, token} = ElixirAuthGoogle.get_token(code, MyAppWeb.Endpoint.url()) 151 | {:ok, profile} = ElixirAuthGoogle.get_user_profile(token.access_token) 152 | conn 153 | |> put_view(AppWeb.PageView) 154 | |> render(:welcome, profile: profile) 155 | end 156 | end 157 | ``` 158 | This code does 3 things: 159 | + Create a one-time auth `token` based on the response `code` sent by Google 160 | after the person authenticates. 161 | + Request the person's profile data from Google based on the `access_token` 162 | + Render a `:welcome` view displaying some profile data 163 | to confirm that login with Google was successful. 164 | 165 | 166 | ## 5. Create the `/auth/google/callback` Endpoint 📍 167 | 168 | Open your **`router.ex`** file 169 | and locate the section that looks like `scope "/", AppWeb do` 170 | 171 | Add the following line: 172 | 173 | ```elixir 174 | get "/auth/google/callback", GoogleAuthController, :index 175 | ``` 176 | 177 | Sample: [lib/app_web/router.ex#L20](https://github.com/dwyl/elixir-auth-google-demo/blob/4bb616dd134f498b84f079104c0f3345769517c4/lib/app_web/router.ex#L20) 178 | 179 | ### Different callback url? 180 | 181 | You can specify the env var 182 | 183 | ``` 184 | export GOOGLE_CALLBACK_PATH=/myauth/google_callback 185 | ``` 186 | 187 | or add it in the configuration 188 | 189 | Or add the following in the config file: 190 | 191 | ```elixir 192 | config :elixir_auth_google, 193 | # ... 194 | callback_path: "/myauth/google_callback" 195 | ``` 196 | 197 | ## 6. Add the "Login with Google" Button to your Template ✨ 198 | 199 | In order to display the "Sign-in with Google" button in the UI, 200 | we need to _generate_ the URL for the button in the relevant controller, 201 | and pass it to the template. 202 | 203 | Open the `lib/app_web/controllers/page_controller.ex` file 204 | and update the `index` function: 205 | 206 | From: 207 | ```elixir 208 | def index(conn, _params) do 209 | render(conn, "index.html") 210 | end 211 | ``` 212 | 213 | To: 214 | ```elixir 215 | def index(conn, _params) do 216 | oauth_google_url = ElixirAuthGoogle.generate_oauth_url(conn) 217 | render(conn, "index.html",[oauth_google_url: oauth_google_url]) 218 | end 219 | ``` 220 | 221 | You can add extra features the the generated url by passing the state as a string, 222 | or any other query as a map of key/value pairs. 223 | 224 | ```elixir 225 | oauth_google_url = ElixirAuthGoogle.generate_oauth_url(conn, state) 226 | ``` 227 | 228 | Will result in Google Sign In link with `&state=state` added. 229 | And something like: 230 | 231 | ```elixir 232 | oauth_google_url = ElixirAuthGoogle.generate_oauth_url(conn, %{lang: 'pt-BR'}) 233 | ``` 234 | 235 | Will return a url with `lang=pt-BR` included in the sign in request. 236 | 237 | #### _Alternatively_ pass the `url` of your `App` into `generate_oauth_url/1` 238 | 239 | We have noticed that on `fly.io` 240 | where the `Phoenix` App is proxied, 241 | passing the `conn` struct 242 | to `ElixirAuthGoogle.generate_oauth_url/2` 243 | is not effective. 244 | See [dwyl/elixir-auth-google/issues/94](https://github.com/dwyl/elixir-auth-google/issues/94) 245 | 246 | So we added an alternative way 247 | of invoking `generate_oauth_url/2` 248 | passing in the `url` of your `App`: 249 | 250 | ```elixir 251 | def index(conn, _params) do 252 | base_url = MyAppWeb.Endpoint.url() 253 | oauth_google_url = ElixirAuthGoogle.generate_oauth_url(base_url) 254 | render(conn, "index.html",[oauth_google_url: oauth_google_url]) 255 | end 256 | ``` 257 | 258 | This uses 259 | [Phoenix.Endpoint.url/0](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#c:url/0) 260 | which is available in any `Phoenix` App. 261 | 262 | Just remember to replace `MyAppWeb` with the name of your `App`. 😉 263 | 264 |
265 | 266 | ### 6.1 Update the `page/index.html.eex` Template 267 | 268 | Open the `/lib/app_web/templates/page/index.html.eex` file 269 | and type the following code: 270 | 271 | ```html 272 |
273 |

Welcome to Awesome App!

274 |

To get started, login to your Google Account:

275 | 276 | Sign in with Google 277 | 278 |

279 | ``` 280 | 281 | # _Done_! 🚀 282 | 283 | The home page of the app now has a big "Sign in with Google" button: 284 | 285 | ![sign-in-button](https://user-images.githubusercontent.com/194400/70202961-3c32c880-1713-11ea-9737-9121030ace06.png) 286 | 287 | When the person clicks the button, 288 | and authenticates with their Google Account, 289 | they will be returned to your App 290 | where you can display a "login success" message: 291 | 292 | ![welcome](https://user-images.githubusercontent.com/194400/70201692-494db880-170f-11ea-9776-0ffd1fdf5a72.png) 293 | 294 | 295 | ### _Optional_: Scopes 296 | 297 | Most of the time you will only want/need 298 | the person's email address and profile data 299 | when authenticating with your App. 300 | In the cases where you need more specific access 301 | to a Google service, you will need to specify the exact scopes. 302 | See: 303 | https://developers.google.com/identity/protocols/oauth2/scopes 304 | 305 | Once you know the scope(s) your App needs access to, 306 | simply define them using an environment variable, e.g: 307 | 308 | ``` 309 | GOOGLE_SCOPE="email contacts photoslibrary" 310 | ``` 311 | Those double-quotes (`"`) encapsulating the environment variable `String` are important. 312 | Without them your system will only assign the first word to the `GOOGLE_SCOPE`. 313 | e.g: `email` and ignore the remaining scopes you need. 314 | ***or*** you can set them as a config variable if you prefer: 315 | 316 | ``` 317 | config :elixir_auth_google, 318 | :google_scope: "email contacts photoslibrary" 319 | ``` 320 | 321 | With that configured, your App will gain access to the requested services 322 | once the person authenticates/authorizes. 323 | 324 |

325 | 326 | 327 | ## _Optimised_ SVG+CSS Button 328 | 329 | In **step 6.1** above, we suggest using an `` 330 | for the `Sign in with GitHub` button. 331 | 332 | But even though this image appears small **`389 × 93 px`** 333 | https://i.imgur.com/Kagbzkq.png it is "only" **`8kb`**: 334 | 335 | ![google-button-8kb](https://user-images.githubusercontent.com/194400/73607428-cd0c1000-45ad-11ea-8639-ffc3e9a0e0a2.png) 336 | 337 | We could spend some time in a graphics editor optimising the image, 338 | but we _know_ we can do better by using our `CSS` skills! 💡 339 | 340 | > **Note**: This is the _official_ button provided by Google: 341 | [developers.google.com/identity/images/signin-assets.zip](developers.google.com/identity/images/signin-assets.zip)
342 | So if there was any optimisation they could squeeze out of it, 343 | they probably would have done it before publishing the zip! 344 | 345 | The following code re-creates the `` 346 | using the GitHub logo **`SVG`** 347 | and `CSS` for layout/style: 348 | 349 | ```html 350 |
351 | 352 | 353 | 358 |
359 | 361 | 362 | 363 | 364 | 365 | 366 |
367 |
368 | Sign in with Google 369 |
370 |
371 |
372 | ``` 373 | 374 | > We created this from scratch using the SVG of the Google logo 375 | and some basic CSS.
376 | For the "making of" journey see: 377 | https://github.com/dwyl/elixir-auth-google/issues/25 378 | 379 | The result looks _better_ than the `` button: 380 | 381 | ![img-vs-svg-8kb-1kb](https://user-images.githubusercontent.com/194400/73607841-54a84d80-45b3-11ea-9d0c-a81005a0bfde.png) 382 | 383 | It can be scaled to any screen size so it will _always_ look great!
384 | Using http://bytesizematters.com we see that our SVG+CSS button is only **`1kb`**: 385 | ![bytesize-matters-google-button](https://user-images.githubusercontent.com/194400/73607378-4fe09b00-45ad-11ea-9ab1-3b383c1d4516.png) 386 | 387 | 388 | That is an **87.5%** bandwidth saving 389 | on the **`8kb`** of the 390 | [**`.png`** button](https://github.com/dwyl/elixir-auth-google/issues/25). 391 | And what's _more_ it reduces the number of HTTP requests 392 | which means the page loads _even_ faster. 393 | 394 | This is used in the Demo app: 395 | [`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) 396 | 397 | 398 | ### `i18n` 399 | 400 | The _biggest_ advantage of having an SVG+CSS button 401 | is that you can _translate_ the button text!
402 | Since the text/copy of the button is now _just_ text in standard HTML, 403 | the user's web browser can _automatically_ translate it!
404 | e.g: _French_ 🇬🇧 > 🇫🇷 405 | 406 | ![google-login-french-translation](https://user-images.githubusercontent.com/194400/73607961-c03eea80-45b4-11ea-9840-5d5f02ff8a13.png) 407 | 408 | This is _much_ better UX for the **80%** of people in the world 409 | who do _not_ speak English _fluently_. 410 | The _single_ biggest engine for growth in startup companies 411 | is [_translating_](https://youtu.be/T9ikpoF2GH0?t=463) 412 | their user interface into more languages. 413 | Obviously don't focus on translations 414 | while you're building your MVP, 415 | but if it's no extra _work_ 416 | to use this SVG+CSS button 417 | and it means the person's web browser 418 | can _automatically_ localise your App! 419 | 420 | ### _Accessibility_ 421 | 422 | The `SVG+CSS` button is more accessible than the image. 423 | Even thought the `` had an `alt` attribute 424 | which is a lot better than nothing, 425 | the `SVG+CSS` button can be re-interpreted 426 | by a non-screen device and more easily transformed. 427 | 428 | 429 |

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

451 | 452 | ## Notes 📝 453 | 454 | + Official Docs for Google Identity Platform: 455 | https://developers.google.com/identity/choose-auth 456 | + Web specific sample code (JS): 457 | https://developers.google.com/identity/sign-in/web 458 | + Google Sign-In for server-side apps: 459 | https://developers.google.com/identity/sign-in/web/server-side-flow 460 | + Using OAuth 2.0 for Web Server Applications: 461 | https://developers.google.com/identity/protocols/OAuth2WebServer 462 | + Google Auth Branding Guidelines: 463 | https://developers.google.com/identity/branding-guidelines
464 | Only two colors are permitted for the button: 465 | **white** `#FFFFFF` and **blue** `#4285F4` 466 | 467 | ![two-colors-of-google-auth-button](https://user-images.githubusercontent.com/194400/69634312-d9be3600-1049-11ea-9354-cdaa53f5c42b.png) 468 | 469 | 470 | ### Fun Facts 📈📊 471 | 472 | Unlike other "social media" companies, 473 | Google/Alphabet does not report it's 474 | _Monthly_ Active Users (MAUs) 475 | or _Daily_ Active Users (DAUs) 476 | however they do release stats in drips 477 | in their Google IO or YouTube events. 478 | The following is a quick list of facts 479 | that make adding Google Auth to your App 480 | a compelling business case: 481 | 482 | + As of May 2019, there are over 483 | [2.5 Billion](https://www.theverge.com/2019/5/7/18528297/google-io-2019-android-devices-play-store-total-number-statistic-keynote) 484 | _active_ Android devices; 485 | [87%](https://www.idc.com/promo/smartphone-market-share/os) global market share. 486 | All these people have Google Accounts in order to use Google services. 487 | + YouTube has 488 | [2 billion](https://www.businessofapps.com/data/youtube-statistics/) 489 | monthly active YouTube users (_signed in with a Google Account_). 490 | + Gmail has 491 | [1.5 Billion](https://www.thenewsminute.com/article/googles-gmail-turns-15-now-has-over-15-billion-monthly-active-users-99275) 492 | monthly active users a 493 | [27% share](https://seotribunal.com/blog/google-stats-and-facts) 494 | of the global email client market. 495 | + [65%](https://techjury.net/stats-about/gmail-statistics) 496 | of Small and Medium sized businesses use Google Apps for business. 497 | + [90%+](https://techjury.net/stats-about/gmail-statistics) 498 | of startups use Gmail. This is a good _proxy_ for "early adopters". 499 | + [68%](https://eu.azcentral.com/story/opinion/op-ed/joannaallhands/2017/10/09/google-classroom-changing-teachers-students-education/708246001/) 500 | of schools in the US use Google Classroom and related G-suite products.
501 | So the _next_ generation of internet/app users have Google accounts. 502 | + Google has 503 | [90.46%](https://seotribunal.com/blog/google-stats-and-facts/) 504 | of the search engine market share worldwide. 95.4% on Mobile. 505 | 506 | Of the 4.5 billion internet users (58% of the world population), 507 | around 3.2 billion (72%) have a Google account. 508 | 90%+ of tech "early adopters" use Google Apps 509 | which means that adding Google OAuth Sign-in 510 | is the _logical_ choice for _most_ Apps. 511 | 512 | ### Privacy Concerns? 🔐 513 | 514 | A _common misconception_ is that adding Google Auth Sign-in 515 | sends a user's application data to Google. 516 | This is **`false`** and App developers have 100% control 517 | over what data is sent to (stored by) Google. 518 | An App can use Google Auth to _authenticate_ a person 519 | (_identify them and get read-only access 520 | to their personal details like **first name** and **email address**_) 521 | without sending any data to Google. 522 | Yes, it will mean that Google "knows" that the person is _using_ your App, 523 | but it will not give Google any insight into _how_ they are using it 524 | or what types of data they are storing in the App. Privacy is maintained. 525 | So if you use the @dwyl app to plan your wedding or next holiday, 526 | Google will not have _any_ of that data 527 | and will not serve any annoying ads based on your project/plans. 528 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() == :test do 4 | import_config "test.exs" 5 | end 6 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Mix.Config 2 | 3 | config :elixir_auth_google, 4 | client_id: "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com", 5 | client_secret: "MHxv6-RGF5nheXnxh1b0LNDq", 6 | httpoison_mock: true 7 | 8 | System.put_env( 9 | "GOOGLE_CLIENT_ID", 10 | "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com" 11 | ) 12 | -------------------------------------------------------------------------------- /create-google-app-guide.md: -------------------------------------------------------------------------------- 1 | # Creating a Google Application for OAuth2 Authentication 2 | 3 | This is a step-by-step guide 4 | for creating a Google App from scratch 5 | so that you can obtain the API keys 6 | to add Google OAuth2 Authentication 7 | to your Elixir App 8 | and save the credentials to environment variables.
9 | Our guide follows the _official_ docs: 10 | https://developers.google.com/identity/sign-in/web/server-side-flow
11 | We've added detail and screenshots to the steps 12 | because some people have found the official Google API docs confusing.
13 | _This_ guide is checked periodically by the @dwyl team/community, 14 | but Google are known to occasionally change their UI/Workflow, 15 | so if anything has changed, or there are extra/fewer steps, 16 | [please let us know!](https://github.com/dwyl/elixir-auth-google/issues) 17 | 18 | ## 1. Create a New Project 19 | 20 | In your preferred web browser, 21 | visit: 22 | https://console.developers.google.com 23 | and ensure you are authenticated with your Google Account 24 | so you can see your "API & Services Dashboard": 25 | 26 | elixir-auth-google-create-new-app 27 | 28 | If you don't already have a Google APIs project for your Elixir App, 29 | click the **CREATE** button on the dashboard. 30 | 31 | 32 | ## 2. Define the Details for your New Project (App) 33 | 34 | Enter the details for your App's **Project name** 35 | and where appropriate input any additional/relevant info: 36 | 37 | elixir-auth-google-app-details 38 | 39 | Click the **CREATE** button to create your project. 40 | 41 | 42 | ## 3. OAuth Consent Screen 43 | 44 | After creating the New Project, 45 | the UI will return to the APIs dashboard 46 | and the name of your app will appear in the top menu. 47 | 48 | Click the **OAuth Consent Screen** from the left side menu: 49 | 50 | elixir-auth-google-consent-screen 51 | 52 | Make the Application **`Public`** (_the default option_) and 53 | input the same name as you used for your application in step 1. 54 | Upload an image if you have one (_e.g: the icon/logo for your app_): 55 | 56 | OAuth-consent-screen-1of2 57 | 58 | Leave the "**Scopes for Google APIs**" set to the default 59 | **email**, **profile** and **openid**. 60 | 61 | No other data is required at this point, so skip the rest. 62 | 63 | Scroll down to the bottom and click "**Save**": 64 | OAuth-consent-screen-2of2 65 | 66 | This will take you to the API Credentials page. 67 | 68 | ## 4. Create Credentials 69 | 70 | Click the **Create Credentials** button: 71 | Screenshot 2019-11-26 at 23 24 28 72 | That will popup a menu from which you will select **OAuth Client ID**. 73 | 74 | You will see a form that allows you to specify 75 | the details of your App for the credentials. 76 | 77 | Screenshot 2019-11-27 at 02 13 55 78 | 79 | + Application Type: Web application 80 | + Name: Elixir Auth Server 81 | + Authorized JavaScript origins: 82 | http://localhost:4000 83 | (_the default for a Phoenix app on your local dev machine. 84 | you can add your "production" URL later._) 85 | + Authorized redirect URIs: 86 | http://localhost:4000/auth/google/callback 87 | (_the endpoint to redirect to once authentication is successful. 88 | again, add your production URL once you have auth working on `localhost`_) 89 | 90 | > Ensure you hit the enter key after pasting/typing 91 | the URIs to ensure they are saved. 92 | A common stumbling block is that URIs aren't saved. See: 93 | https://stackoverflow.com/questions/24363041/redirect-uri-in-google-cloud-console-doesnt-save 94 | 95 | Once you have input the relevant data click the **Create** button. 96 | 97 | > This form/step can be confusing at first, 98 | but essentially you can have multiple credentials 99 | for the same project, 100 | e.g: if you had a Native Android App 101 | you would create a new set of credentials 102 | to ensure a separation of concerns between 103 | server and client implementations. 104 | For now just create the server (Elixir) credentials. 105 | 106 | 107 | ## 5. Download the OAuth Client Credentials 108 | 109 | After you click the **Create** button 110 | in the **Create OAuth client ID** form (_step 4 above_), 111 | you will be shown your OAuth client Credentials: 112 | 113 | elixir-auth-google-oauth-client-credentials 114 | 115 | Download the credentials, e.g: 116 | 117 | + Client ID: 631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com 118 | + Client Secret: MHxv6-RGF5nheXnxh1b0LNDq 119 | 120 | > ⚠️ Don't worry, these keys aren't valid. 121 | We deleted them immediately after capturing the screenshot 122 | to avoid any security issues. 123 | Obviously treat your credentials 124 | like you would the username+password for your bank account; 125 | never share a **real** Client ID or secret on GitHub 126 | or any other public/insecure forum! 127 | 128 | You can also download the OAuth credentials as a json file: 129 | 130 | elixir-auth-google-json 131 | 132 | Example: 133 | ```json 134 | { 135 | "web": { 136 | "client_id": "631770888008-6n0oruvsm16kbkqg6u76p5cv5kfkcekt.apps.googleusercontent.com", 137 | "project_id": "elixir-auth-demo", 138 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 139 | "token_uri": "https://oauth2.googleapis.com/token", 140 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 141 | "client_secret": "MHxv6-RGF5nheXnxh1b0LNDq", 142 | "redirect_uris": [ 143 | "http://localhost:4000/auth/google/callback" 144 | ], 145 | "javascript_origins": [ 146 | "http://localhost:4000" 147 | ] 148 | } 149 | } 150 | ``` 151 | 152 | > Again, for security reasons, 153 | these credentials were 154 | invalidated _immediately_ after downloading.
155 | But this is what the file looks like. 156 | 157 | 158 | Return to step 3 of the 159 | [README.md](https://github.com/dwyl/elixir-auth-google/blob/master/README.md) 160 | 161 | 162 |
163 | 164 | # Note 165 | 166 | When you ship your app to your Production environment, 167 | you will need to re-visit steps 3 & 4 168 | to update your app settings URL & callback 169 | to reflect the URl where you are deploying your app e.g: 170 | 171 | ![add-heroku-app](https://user-images.githubusercontent.com/194400/70204921-32f92a00-171a-11ea-83b2-34e5eeea777b.png) 172 | 173 | In our case 174 | https://elixir-auth-google-demo.herokuapp.com 175 | and 176 | https://elixir-auth-google-demo.herokuapp.com/auth/google/callback 177 | -------------------------------------------------------------------------------- /lib/elixir_auth_google.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAuthGoogle do 2 | @moduledoc """ 3 | Minimalist Google OAuth Authentication for Elixir Apps. 4 | Extensively tested, documented, maintained and in active use in production. 5 | """ 6 | @google_auth_url "https://accounts.google.com/o/oauth2/v2/auth?response_type=code" 7 | @google_token_url "https://oauth2.googleapis.com/token" 8 | @google_user_profile "https://www.googleapis.com/oauth2/v3/userinfo" 9 | @default_scope "profile email" 10 | @default_callback_path "/auth/google/callback" 11 | 12 | @httpoison (Application.compile_env(:elixir_auth_google, :httpoison_mock) && 13 | ElixirAuthGoogle.HTTPoisonMock) || HTTPoison 14 | 15 | @type conn :: map 16 | @type url :: String.t() 17 | 18 | @doc """ 19 | `inject_poison/0` injects a TestDouble of HTTPoison in Test 20 | so that we don't have duplicate mock in consuming apps. 21 | see: github.com/dwyl/elixir-auth-google/issues/35 22 | """ 23 | def inject_poison, do: @httpoison 24 | 25 | @doc """ 26 | `get_baseurl_from_conn/1` derives the base URL from the conn struct 27 | """ 28 | @spec get_baseurl_from_conn(conn) :: String.t() 29 | def get_baseurl_from_conn(%{host: h, port: p, scheme: s}) when p != 80 do 30 | "#{Atom.to_string(s)}://#{h}:#{p}" 31 | end 32 | 33 | def get_baseurl_from_conn(%{host: h, scheme: s}) do 34 | "#{Atom.to_string(s)}://#{h}" 35 | end 36 | 37 | def get_baseurl_from_conn(%{host: h} = conn) do 38 | scheme = 39 | case h do 40 | "localhost" -> :http 41 | _ -> :https 42 | end 43 | 44 | get_baseurl_from_conn(Map.put(conn, :scheme, scheme)) 45 | end 46 | 47 | @doc """ 48 | `generate_redirect_uri/1` generates the Google redirect uri based on `conn` 49 | or the `url`. If the `App.Endpoint.url()` 50 | e.g: auth.dwyl.com or https://gcal.fly.dev 51 | is passed into `generate_redirect_uri/1`, 52 | return that `url` with the callback appended to it. 53 | See: github.com/dwyl/elixir-auth-google/issues/94 54 | """ 55 | @spec generate_redirect_uri(url) :: String.t() 56 | def generate_redirect_uri(url) when is_binary(url) do 57 | scheme = 58 | cond do 59 | # url already contains scheme return empty 60 | String.contains?(url, "https") -> "" 61 | # url contains ":" is localhost:4000 no need for scheme 62 | String.contains?(url, ":") -> "" 63 | # Default to https if scheme not set e.g: app.fly.dev -> https://app.fly.fev 64 | true -> "https://" 65 | end 66 | 67 | "#{scheme}#{url}" <> get_app_callback_url() 68 | end 69 | 70 | @spec generate_redirect_uri(conn) :: String.t() 71 | def generate_redirect_uri(conn) do 72 | get_baseurl_from_conn(conn) <> get_app_callback_url() 73 | end 74 | 75 | @doc """ 76 | `generate_oauth_url/1` creates the Google OAuth2 URL with client_id, scope and 77 | redirect_uri which is the URL Google will redirect to when auth is successful. 78 | This is the URL you need to use for your "Login with Google" button. 79 | See step 5 of the instructions. 80 | """ 81 | @spec generate_oauth_url(String.t()) :: String.t() 82 | def generate_oauth_url(url) when is_binary(url) do 83 | query = %{ 84 | client_id: google_client_id(), 85 | scope: google_scope(), 86 | redirect_uri: generate_redirect_uri(url) 87 | } 88 | 89 | params = URI.encode_query(query, :rfc3986) 90 | 91 | "#{@google_auth_url}&#{params}" 92 | end 93 | 94 | @spec generate_oauth_url(conn) :: String.t() 95 | def generate_oauth_url(conn) when is_map(conn) do 96 | query = %{ 97 | client_id: google_client_id(), 98 | scope: google_scope(), 99 | redirect_uri: generate_redirect_uri(conn) 100 | } 101 | 102 | params = URI.encode_query(query, :rfc3986) 103 | 104 | "#{@google_auth_url}&#{params}" 105 | end 106 | 107 | @doc """ 108 | Same as `generate_oauth_url/1` with `state` query parameter, 109 | or a `map` of key/pair values to be included in the urls query string. 110 | """ 111 | @spec generate_oauth_url(conn, String.t() | map) :: String.t() 112 | def generate_oauth_url(conn, state) when is_binary(state) do 113 | params = URI.encode_query(%{state: state}, :rfc3986) 114 | generate_oauth_url(conn) <> "&#{params}" 115 | end 116 | 117 | def generate_oauth_url(conn, query) when is_map(query) do 118 | query = URI.encode_query(query, :rfc3986) 119 | generate_oauth_url(conn) <> "&#{query}" 120 | end 121 | 122 | @doc """ 123 | `get_token/2` encodes the secret keys and authorization code returned by Google 124 | and issues an HTTP request to get a person's profile data. 125 | 126 | **TODO**: we still need to handle the various failure conditions >> issues/16 127 | """ 128 | @spec get_token(String.t(), conn) :: {:ok, map} | {:error, any} 129 | def get_token(code, conn) when is_map(conn) do 130 | redirect_uri = generate_redirect_uri(conn) 131 | 132 | inject_poison().post(@google_token_url, req_body(code, redirect_uri)) 133 | |> parse_body_response() 134 | end 135 | 136 | @spec get_token(String.t(), url) :: {:ok, map} | {:error, any} 137 | def get_token(code, url) when is_binary(url) do 138 | redirect_uri = generate_redirect_uri(url) 139 | 140 | inject_poison().post(@google_token_url, req_body(code, redirect_uri)) 141 | |> parse_body_response() 142 | end 143 | 144 | defp req_body(code, redirect_uri) do 145 | Jason.encode!(%{ 146 | client_id: google_client_id(), 147 | client_secret: google_client_secret(), 148 | redirect_uri: redirect_uri, 149 | grant_type: "authorization_code", 150 | code: code 151 | }) 152 | end 153 | 154 | @doc """ 155 | `get_user_profile/1` requests the Google User's userinfo profile data 156 | providing the access_token received in the `get_token/1` above. 157 | invokes `parse_body_response/1` to decode the JSON data. 158 | 159 | **TODO**: we still need to handle the various failure conditions >> issues/16 160 | At this point the types of errors we expect are HTTP 40x/50x responses. 161 | """ 162 | @spec get_user_profile(String.t()) :: {:ok, map} | {:error, any} 163 | def get_user_profile(token) do 164 | params = URI.encode_query(%{access_token: token}, :rfc3986) 165 | 166 | "#{@google_user_profile}?#{params}" 167 | |> inject_poison().get() 168 | |> parse_body_response() 169 | end 170 | 171 | @doc """ 172 | `parse_body_response/1` parses the response returned by Google 173 | so your app can use the resulting JSON. 174 | """ 175 | @spec parse_body_response({atom, String.t()} | {:error, any}) :: {:ok, map} | {:error, any} 176 | def parse_body_response({:error, err}), do: {:error, err} 177 | 178 | def parse_body_response({:ok, response}) do 179 | body = Map.get(response, :body) 180 | # make keys of map atoms for easier access in templates 181 | if body == nil do 182 | {:error, :no_body} 183 | else 184 | {:ok, str_key_map} = Jason.decode(body) 185 | atom_key_map = for {key, val} <- str_key_map, into: %{}, do: {String.to_atom(key), val} 186 | {:ok, atom_key_map} 187 | end 188 | 189 | # https://stackoverflow.com/questions/31990134 190 | end 191 | 192 | def google_client_id do 193 | System.get_env("GOOGLE_CLIENT_ID") || Application.get_env(:elixir_auth_google, :client_id) 194 | end 195 | 196 | defp google_client_secret do 197 | System.get_env("GOOGLE_CLIENT_SECRET") || 198 | Application.get_env(:elixir_auth_google, :client_secret) 199 | end 200 | 201 | defp google_scope do 202 | System.get_env("GOOGLE_SCOPE") || Application.get_env(:elixir_auth_google, :google_scope) || 203 | @default_scope 204 | end 205 | 206 | defp get_app_callback_url do 207 | System.get_env("GOOGLE_CALLBACK_PATH") || 208 | Application.get_env(:elixir_auth_google, :callback_path) || @default_callback_path 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/httpoison_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAuthGoogle.HTTPoisonMock do 2 | @moduledoc """ 3 | This is a TestDouble for HTTPoison which returns a predictable response. 4 | Please see: https://github.com/dwyl/elixir-auth-google/issues/35 5 | """ 6 | 7 | @doc """ 8 | get/1 passing in the wrong_token is used to test failure in the auth process. 9 | Obviously, don't invoke it from your App unless you want people to see fails. 10 | """ 11 | def get("https://www.googleapis.com/oauth2/v3/userinfo?access_token=wrong_token") do 12 | {:error, :bad_request} 13 | end 14 | 15 | # get/1 using a dummy _url to test body decoding. 16 | def get(_url) do 17 | {:ok, 18 | %{ 19 | body: 20 | Jason.encode!(%{ 21 | email: "nelson@gmail.com", 22 | email_verified: true, 23 | family_name: "Correia", 24 | given_name: "Nelson", 25 | locale: "en", 26 | name: "Nelson Correia", 27 | picture: "https://lh3.googleusercontent.com/a-/AAuE7mApnYb260YC1JY7a", 28 | sub: "940732358705212133793" 29 | }) 30 | }} 31 | end 32 | 33 | @doc """ 34 | post/2 passing in dummy _url & _body to test return of access_token. 35 | """ 36 | def post(_url, _body) do 37 | {:ok, %{body: Jason.encode!(%{access_token: "token1"})}} 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAuthGoogle.MixProject do 2 | use Mix.Project 3 | 4 | @description "Minimalist Google OAuth Authentication for Elixir Apps" 5 | @version "1.6.10" 6 | 7 | def project do 8 | [ 9 | app: :elixir_auth_google, 10 | version: @version, 11 | elixir: ">= 1.11.0", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | description: @description, 15 | package: package(), 16 | aliases: aliases(), 17 | # coverage 18 | test_coverage: [tool: ExCoveralls], 19 | preferred_cli_env: [ 20 | c: :test, 21 | coveralls: :test, 22 | "coveralls.json": :test, 23 | "coveralls.html": :test, 24 | t: :test 25 | ] 26 | ] 27 | end 28 | 29 | # Run "mix help compile.app" to learn about applications. 30 | def application do 31 | [ 32 | extra_applications: [:logger] 33 | ] 34 | end 35 | 36 | # Run "mix help deps" to learn about dependencies. 37 | defp deps do 38 | [ 39 | {:credo, "~> 1.5", only: [:dev, :test], runtime: false}, 40 | {:httpoison, "~> 2.2.0"}, 41 | {:jason, "~> 1.2"}, 42 | 43 | # Track test coverage: github.com/parroty/excoveralls 44 | {:excoveralls, "~> 0.18.0", only: [:test, :dev]}, 45 | 46 | # Mock stuffs in test: github.com/jjh42/mock 47 | {:mock, "~> 0.3.0", only: :test}, 48 | 49 | # documentation 50 | {:ex_doc, "~> 0.38.2", only: :dev} 51 | ] 52 | end 53 | 54 | defp package do 55 | [ 56 | maintainers: ["dwyl"], 57 | licenses: ["GPL-2.0-or-later"], 58 | links: %{github: "https://github.com/dwyl/elixir-auth-google"}, 59 | files: ~w(lib LICENSE mix.exs README.md .formatter.exs) 60 | ] 61 | end 62 | 63 | defp aliases do 64 | [ 65 | t: ["test"], 66 | c: ["coveralls.html"] 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, 4 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 5 | "earmark": {:hex, :earmark, "1.4.9", "837e4c1c5302b3135e9955f2bbf52c6c52e950c383983942b68b03909356c0d9", [:mix], [{:earmark_parser, ">= 1.4.9", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "0d72df7d13a3dc8422882bed5263fdec5a773f56f7baeb02379361cb9e5b0d8e"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 7 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 8 | "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, 9 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 10 | "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.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.4.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", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, 11 | "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, 12 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 13 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 14 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 15 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 16 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 17 | "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, 18 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 19 | "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, 20 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, 21 | "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, 22 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 23 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 24 | "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [: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", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, 25 | "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, 26 | "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, 27 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 28 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 29 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 30 | } 31 | -------------------------------------------------------------------------------- /test/elixir_auth_google_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAuthGoogleTest do 2 | use ExUnit.Case, async: true 3 | doctest ElixirAuthGoogle 4 | 5 | import Mock 6 | 7 | test "get_baseurl_from_conn(conn) detects the URL based on conn.host HTTP" do 8 | conn = %{ 9 | host: "localhost", 10 | port: 4000, 11 | scheme: :http 12 | } 13 | 14 | assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "http://localhost:4000" 15 | end 16 | 17 | test "get_baseurl_from_conn(conn) detects the URL based on conn.host HTTPS" do 18 | conn = %{ 19 | host: "localhost", 20 | port: 4000, 21 | scheme: :https 22 | } 23 | 24 | assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "https://localhost:4000" 25 | end 26 | 27 | test "get_baseurl_from_conn(conn) detects the URL for production HTTPS" do 28 | conn = %{ 29 | host: "dwyl.com", 30 | port: 80, 31 | scheme: :https 32 | } 33 | 34 | assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "https://dwyl.com" 35 | end 36 | 37 | test "get_baseurl_from_conn(conn) detects the URL for production HTTP" do 38 | conn = %{ 39 | host: "dwyl.com", 40 | port: 80, 41 | scheme: :http 42 | } 43 | 44 | assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "http://dwyl.com" 45 | end 46 | 47 | test "get_baseurl_from_conn(conn) detects the URL for production HTTPS, non-standard port" do 48 | conn = %{ 49 | host: "dwyl.com", 50 | port: 8080, 51 | scheme: :https 52 | } 53 | 54 | assert ElixirAuthGoogle.get_baseurl_from_conn(conn) == "https://dwyl.com:8080" 55 | end 56 | 57 | test "get Google login url" do 58 | conn = %{ 59 | host: "localhost", 60 | port: 4000 61 | } 62 | 63 | assert ElixirAuthGoogle.generate_oauth_url(conn) =~ 64 | "https://accounts.google.com/o/oauth2/v2/auth?response_type=code" 65 | end 66 | 67 | test "get Google login url (config redirect uri)" do 68 | conn = %{ 69 | host: "localhost", 70 | port: 4000 71 | } 72 | 73 | url = ElixirAuthGoogle.generate_oauth_url(conn) 74 | assert url =~ "https://accounts.google.com/o/oauth2/v2/auth?response_type=code" 75 | assert url =~ "http%3A%2F%2Flocalhost%3A4000" 76 | end 77 | 78 | test "get Google login url with state" do 79 | conn = %{ 80 | host: "localhost", 81 | port: 4000 82 | } 83 | 84 | url = ElixirAuthGoogle.generate_oauth_url(conn, "state1") 85 | id = System.get_env("GOOGLE_CLIENT_ID") 86 | id_from_config = Application.get_env(:elixir_auth_google, :client_id) 87 | 88 | assert id == id_from_config 89 | 90 | expected = 91 | "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=" <> 92 | id <> 93 | "&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email&state=state1" 94 | 95 | assert url == expected 96 | end 97 | 98 | test "get Google login with language parameters" do 99 | conn = %{ 100 | host: "localhost", 101 | port: 4000 102 | } 103 | 104 | url = ElixirAuthGoogle.generate_oauth_url(conn, %{hl: "es-MX"}) 105 | id = System.get_env("GOOGLE_CLIENT_ID") 106 | id_from_config = Application.get_env(:elixir_auth_google, :client_id) 107 | 108 | assert id == id_from_config 109 | 110 | expected = 111 | "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=" <> 112 | id <> 113 | "&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email&hl=es-MX" 114 | 115 | assert url == expected 116 | end 117 | 118 | test "get Google token" do 119 | conn = %{ 120 | host: "localhost", 121 | port: 4000 122 | } 123 | 124 | {:ok, res} = ElixirAuthGoogle.get_token("ok_code", conn) 125 | assert res == %{access_token: "token1"} 126 | end 127 | 128 | test "get Google token with url as second param #94" do 129 | {:ok, res} = ElixirAuthGoogle.get_token("ok_code", "gcal.fly.dev") 130 | assert res == %{access_token: "token1"} 131 | end 132 | 133 | test "get Google token (config redirect uri)" do 134 | conn = %{ 135 | host: "localhost", 136 | port: 4000 137 | } 138 | 139 | {:ok, res} = ElixirAuthGoogle.get_token("ok_code", conn) 140 | assert res == %{access_token: "token1"} 141 | end 142 | 143 | test "get_user_profile/1" do 144 | res = %{ 145 | email: "nelson@gmail.com", 146 | email_verified: true, 147 | family_name: "Correia", 148 | given_name: "Nelson", 149 | locale: "en", 150 | name: "Nelson Correia", 151 | picture: "https://lh3.googleusercontent.com/a-/AAuE7mApnYb260YC1JY7a", 152 | sub: "940732358705212133793" 153 | } 154 | 155 | assert ElixirAuthGoogle.get_user_profile("123") == {:ok, res} 156 | end 157 | 158 | test "return error with incorrect token" do 159 | assert ElixirAuthGoogle.get_user_profile("wrong_token") == {:error, :bad_request} 160 | end 161 | 162 | test "generate_redirect_uri(conn) generate correct callback url" do 163 | conn = %{ 164 | host: "foobar.com", 165 | port: 80 166 | } 167 | 168 | assert ElixirAuthGoogle.generate_redirect_uri(conn) == 169 | "https://foobar.com/auth/google/callback" 170 | end 171 | 172 | test "generate_oauth_url(url) passing in App.Endpoint.url() #94" do 173 | url = "gcal.fly.dev" 174 | client_id = ElixirAuthGoogle.google_client_id() 175 | https = "https%3A%2F%2F#{url}" 176 | 177 | auth_url = 178 | "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=#{client_id}&redirect_uri=#{https}%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email" 179 | 180 | assert ElixirAuthGoogle.generate_oauth_url(url) =~ auth_url 181 | end 182 | 183 | test "generate_oauth_url(url) with scheme e.g. https://gcal.fly.dev #94" do 184 | no_scheme = "gcal.fly.dev" 185 | url = "https://#{no_scheme}" 186 | client_id = ElixirAuthGoogle.google_client_id() 187 | https = "https%3A%2F%2F#{no_scheme}" 188 | 189 | auth_url = 190 | "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=#{client_id}&redirect_uri=#{https}%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email" 191 | 192 | assert ElixirAuthGoogle.generate_oauth_url(url) =~ auth_url 193 | end 194 | 195 | test "generate_redirect_uri(conn) generate correct callback url with custom url path from application environment variable" do 196 | conn = %{ 197 | host: "foobar.com", 198 | port: 80 199 | } 200 | 201 | mock_get_env = fn :elixir_auth_google, :callback_path -> "/special/callback" end 202 | 203 | with_mock Application, get_env: mock_get_env do 204 | assert ElixirAuthGoogle.generate_redirect_uri(conn) == 205 | "https://foobar.com/special/callback" 206 | end 207 | end 208 | 209 | test "generate_redirect_uri(conn) generate correct callback url with custom url path from system environment variable" do 210 | conn = %{ 211 | host: "foobar.com", 212 | port: 80 213 | } 214 | 215 | mock_get_env = fn "GOOGLE_CALLBACK_PATH" -> "/very/special/callback" end 216 | 217 | with_mock System, get_env: mock_get_env do 218 | assert ElixirAuthGoogle.generate_redirect_uri(conn) == 219 | "https://foobar.com/very/special/callback" 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------