├── .formatter.exs ├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── ueber_github.ex └── ueberauth │ └── strategy │ ├── github.ex │ └── github │ └── oauth.ex ├── mix.exs ├── mix.lock └── test ├── test_helper.exs └── ueber_github_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Order is important. The last matching pattern takes the most precedence. 2 | # Default owners for everything in the repo. 3 | * @ueberauth/developers 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | qa: 13 | uses: straw-hat-team/github-actions-workflows/.github/workflows/elixir-quality-assurance.yml@v1.6.3 14 | with: 15 | elixir-version: '1.14.2' 16 | otp-version: '25.2' 17 | version-type: 'strict' 18 | credo-enabled: false 19 | dialyzer-enabled: false 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | hex-publish: 9 | uses: straw-hat-team/github-actions-workflows/.github/workflows/elixir-hex-publish.yml@v1.6.3 10 | with: 11 | elixir-version: '1.14.2' 12 | otp-version: '25.2' 13 | version-type: 'strict' 14 | secrets: 15 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/elixir,osx,vim 3 | 4 | ### Elixir ### 5 | /_build 6 | /cover 7 | /deps 8 | /tmp 9 | erl_crash.dump 10 | *.ez 11 | *.beam 12 | 13 | ### Elixir Patch ### 14 | /doc 15 | ### OSX ### 16 | *.DS_Store 17 | .AppleDouble 18 | .LSOverride 19 | 20 | # Icon must end with two \r 21 | Icon 22 | 23 | # Thumbnails 24 | ._* 25 | 26 | # Files that might appear in the root of a volume 27 | .DocumentRevisions-V100 28 | .fseventsd 29 | .Spotlight-V100 30 | .TemporaryItems 31 | .Trashes 32 | .VolumeIcon.icns 33 | .com.apple.timemachine.donotpresent 34 | 35 | # Directories potentially created on remote AFP share 36 | .AppleDB 37 | .AppleDesktop 38 | Network Trash Folder 39 | Temporary Items 40 | .apdisk 41 | 42 | ### Vim ### 43 | # swap 44 | [._]*.s[a-v][a-z] 45 | [._]*.sw[a-p] 46 | [._]s[a-v][a-z] 47 | [._]sw[a-p] 48 | # session 49 | Session.vim 50 | # temporary 51 | .netrwhist 52 | *~ 53 | # auto-generated tag files 54 | tags 55 | 56 | # End of https://www.gitignore.io/api/elixir,osx,vim 57 | 58 | 59 | ### VS Code ### 60 | .elixir_ls 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## v0.8.3 9 | 10 | * Fix empty scope not allowing to proceed with an expected missing email [#77](https://github.com/ueberauth/ueberauth_github/issues/77) 11 | 12 | ## v0.8.2 13 | 14 | * Add user agent header [#76](https://github.com/ueberauth/ueberauth_github/pull/76) 15 | * Update version in README [#70](https://github.com/ueberauth/ueberauth_github/pull/70) 16 | * Fix typos [#72](https://github.com/ueberauth/ueberauth_github/pull/72) 17 | * Relax version constraint on ueberauth [#71](https://github.com/ueberauth/ueberauth_github/pull/71) 18 | 19 | ## v0.8.1 20 | 21 | * Adds the ability to read GitHub client credentials at runtime, rather than only at compile time. 22 | 23 | ## v0.8.0 24 | 25 | * Allow private emails [#42](https://github.com/ueberauth/ueberauth_github/pull/42) 26 | * Use `Ueberauth` JSON library configuration [#51](https://github.com/ueberauth/ueberauth_github/pull/51) 27 | * Add `description` field [#38](https://github.com/ueberauth/ueberauth_github/pull/38) 28 | 29 | ## v0.4.1 30 | 31 | * Use OAuth 0.8 32 | * Fix Elixir warnings 33 | * Replace Map usage with Keyword 34 | 35 | ## v0.2.0 36 | 37 | * Bump to version 0.2.0 of Ueberauth 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ueberauth GitHub 2 | 3 | ## Pull Requests Welcome 4 | 1. Fork ueberauth_github 5 | 2. Create a topic branch 6 | 3. Make logically-grouped commits with clear commit messages 7 | 4. Push commits to your fork 8 | 5. Open a pull request against ueberauth_github/master 9 | 10 | ## Issues 11 | 12 | If you believe there to be a bug, please provide the maintainers with enough 13 | detail to reproduce or a link to an app exhibiting unexpected behavior. For 14 | help, please start with Stack Overflow. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Neighman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Überauth GitHub 2 | 3 | [![Build Status](https://travis-ci.org/ueberauth/ueberauth_github.svg?branch=master)](https://travis-ci.org/ueberauth/ueberauth_github) 4 | [![Module Version](https://img.shields.io/hexpm/v/ueberauth_github.svg)](https://hex.pm/packages/ueberauth_github) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ueberauth_github/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/ueberauth_github.svg)](https://hex.pm/packages/ueberauth_github) 7 | [![License](https://img.shields.io/hexpm/l/ueberauth_github.svg)](https://github.com/ueberauth/ueberauth_github/blob/master/LICENSE.md) 8 | [![Last Updated](https://img.shields.io/github/last-commit/ueberauth/ueberauth_github.svg)](https://github.com/ueberauth/ueberauth_github/commits/master) 9 | 10 | > GitHub OAuth2 strategy for Überauth. 11 | 12 | ## Installation 13 | 14 | 1. Setup your application at [GitHub Developer](https://developer.github.com). 15 | 16 | 2. Add `:ueberauth_github` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:ueberauth_github, "~> 0.8"} 22 | ] 23 | end 24 | ``` 25 | 26 | 3. Add GitHub to your Überauth configuration: 27 | 28 | ```elixir 29 | config :ueberauth, Ueberauth, 30 | providers: [ 31 | github: {Ueberauth.Strategy.Github, []} 32 | ] 33 | ``` 34 | 35 | 4. Update your provider configuration: 36 | 37 | ```elixir 38 | config :ueberauth, Ueberauth.Strategy.Github.OAuth, 39 | client_id: System.get_env("GITHUB_CLIENT_ID"), 40 | client_secret: System.get_env("GITHUB_CLIENT_SECRET") 41 | ``` 42 | 43 | Or, to read the client credentials at runtime: 44 | 45 | ```elixir 46 | config :ueberauth, Ueberauth.Strategy.Github.OAuth, 47 | client_id: {:system, "GITHUB_CLIENT_ID"}, 48 | client_secret: {:system, "GITHUB_CLIENT_SECRET"} 49 | ``` 50 | 51 | 5. Include the Überauth plug in your router: 52 | 53 | ```elixir 54 | defmodule MyApp.Router do 55 | use MyApp.Web, :router 56 | 57 | pipeline :browser do 58 | plug Ueberauth 59 | ... 60 | end 61 | end 62 | ``` 63 | 64 | 6. Create the request and callback routes if you haven't already: 65 | 66 | ```elixir 67 | scope "/auth", MyApp do 68 | pipe_through :browser 69 | 70 | get "/:provider", AuthController, :request 71 | get "/:provider/callback", AuthController, :callback 72 | end 73 | ``` 74 | 75 | 7. Your controller needs to implement callbacks to deal with `Ueberauth.Auth` 76 | and `Ueberauth.Failure` responses. 77 | 78 | For an example implementation see the [Überauth Example](https://github.com/ueberauth/ueberauth_example) application. 79 | 80 | ## Calling 81 | 82 | Depending on the configured url you can initiate the request through: 83 | 84 | /auth/github 85 | 86 | Or with options: 87 | 88 | /auth/github?scope=user,public_repo 89 | 90 | By default the requested scope is `"user,public\_repo"`. This provides both read 91 | and write access to the GitHub user profile details and public repos. For a 92 | read-only scope, either use `"user:email"` or an empty scope `""`. Empty scope 93 | will only request minimum public information which even excludes user's email address 94 | which results in a `nil` for `email` inside returned `%Ueberauth.Auth.Info{}`. 95 | See more at [GitHub's OAuth Documentation](https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-scopes-for-oauth-apps/). 96 | 97 | Scope can be configured either explicitly as a `scope` query value on the 98 | request path or in your configuration: 99 | 100 | ```elixir 101 | config :ueberauth, Ueberauth, 102 | providers: [ 103 | github: {Ueberauth.Strategy.Github, [default_scope: "user,public_repo,notifications"]} 104 | ] 105 | ``` 106 | 107 | It is also possible to disable the sending of the `redirect_uri` to GitHub. 108 | This is particularly useful when your production application sits behind a 109 | proxy that handles SSL connections. In this case, the `redirect_uri` sent by 110 | `Ueberauth` will start with `http` instead of `https`, and if you configured 111 | your GitHub OAuth application's callback URL to use HTTPS, GitHub will throw an 112 | `uri_mismatch` error. 113 | 114 | To prevent `Ueberauth` from sending the `redirect_uri`, you should add the 115 | following to your configuration: 116 | 117 | ```elixir 118 | config :ueberauth, Ueberauth, 119 | providers: [ 120 | github: {Ueberauth.Strategy.Github, [send_redirect_uri: false]} 121 | ] 122 | ``` 123 | 124 | ## Private Emails 125 | 126 | GitHub now allows you to keep your email address private. If you don't mind 127 | that you won't know a users email address you can specify 128 | `allow_private_emails`. This will set the users email as 129 | `id+username@users.noreply.github.com`. 130 | 131 | ```elixir 132 | config :ueberauth, Ueberauth, 133 | providers: [ 134 | github: {Ueberauth.Strategy.Github, [allow_private_emails: true]} 135 | ] 136 | ``` 137 | 138 | ## Copyright and License 139 | 140 | Copyright (c) 2015 Daniel Neighman 141 | 142 | This library is released under the MIT License. See the [LICENSE.md](./LICENSE.md) file 143 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :ueberauth_github, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:ueberauth_github, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/ueber_github.ex: -------------------------------------------------------------------------------- 1 | defmodule UeberauthGithub do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /lib/ueberauth/strategy/github.ex: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Github do 2 | @moduledoc """ 3 | Provides an Ueberauth strategy for authenticating with GitHub. 4 | 5 | ### Setup 6 | 7 | Create an application in Github for you to use. 8 | 9 | Register a new application at: [your github developer page](https://github.com/settings/developers) 10 | and get the `client_id` and `client_secret`. 11 | 12 | Include the provider in your configuration for Ueberauth; 13 | 14 | config :ueberauth, Ueberauth, 15 | providers: [ 16 | github: { Ueberauth.Strategy.Github, [] } 17 | ] 18 | 19 | Then include the configuration for GitHub: 20 | 21 | config :ueberauth, Ueberauth.Strategy.Github.OAuth, 22 | client_id: System.get_env("GITHUB_CLIENT_ID"), 23 | client_secret: System.get_env("GITHUB_CLIENT_SECRET") 24 | 25 | If you haven't already, create a pipeline and setup routes for your callback handler 26 | 27 | pipeline :auth do 28 | Ueberauth.plug "/auth" 29 | end 30 | 31 | scope "/auth" do 32 | pipe_through [:browser, :auth] 33 | 34 | get "/:provider/callback", AuthController, :callback 35 | end 36 | 37 | Create an endpoint for the callback where you will handle the 38 | `Ueberauth.Auth` struct: 39 | 40 | defmodule MyApp.AuthController do 41 | use MyApp.Web, :controller 42 | 43 | def callback_phase(%{ assigns: %{ ueberauth_failure: fails } } = conn, _params) do 44 | # do things with the failure 45 | end 46 | 47 | def callback_phase(%{ assigns: %{ ueberauth_auth: auth } } = conn, params) do 48 | # do things with the auth 49 | end 50 | end 51 | 52 | You can edit the behaviour of the Strategy by including some options when you 53 | register your provider. 54 | 55 | To set the `uid_field`: 56 | 57 | config :ueberauth, Ueberauth, 58 | providers: [ 59 | github: { Ueberauth.Strategy.Github, [uid_field: :email] } 60 | ] 61 | 62 | Default is `:id`. 63 | 64 | To set the default 'scopes' (permissions): 65 | 66 | config :ueberauth, Ueberauth, 67 | providers: [ 68 | github: { Ueberauth.Strategy.Github, [default_scope: "user,public_repo"] } 69 | ] 70 | 71 | Default is empty ("") which "Grants read-only access to public information 72 | (includes public user profile info, public repository info, and gists)" 73 | """ 74 | use Ueberauth.Strategy, 75 | uid_field: :id, 76 | default_scope: "", 77 | send_redirect_uri: true, 78 | oauth2_module: Ueberauth.Strategy.Github.OAuth 79 | 80 | alias Ueberauth.Auth.Info 81 | alias Ueberauth.Auth.Credentials 82 | alias Ueberauth.Auth.Extra 83 | 84 | @doc """ 85 | Handles the initial redirect to the github authentication page. 86 | 87 | To customize the scope (permissions) that are requested by github include 88 | them as part of your url: 89 | 90 | "/auth/github?scope=user,public_repo,gist" 91 | """ 92 | 93 | def handle_request!(conn) do 94 | opts = 95 | [] 96 | |> with_scopes(conn) 97 | |> with_state_param(conn) 98 | |> with_redirect_uri(conn) 99 | 100 | module = option(conn, :oauth2_module) 101 | redirect!(conn, apply(module, :authorize_url!, [opts])) 102 | end 103 | 104 | @doc """ 105 | Handles the callback from GitHub. 106 | 107 | When there is a failure from Github the failure is included in the 108 | `ueberauth_failure` struct. Otherwise the information returned from Github is 109 | returned in the `Ueberauth.Auth` struct. 110 | """ 111 | def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do 112 | module = option(conn, :oauth2_module) 113 | token = apply(module, :get_token!, [[code: code]]) 114 | 115 | if token.access_token == nil do 116 | set_errors!(conn, [ 117 | error(token.other_params["error"], token.other_params["error_description"]) 118 | ]) 119 | else 120 | fetch_user(conn, token) 121 | end 122 | end 123 | 124 | @doc false 125 | def handle_callback!(conn) do 126 | set_errors!(conn, [error("missing_code", "No code received")]) 127 | end 128 | 129 | @doc """ 130 | Cleans up the private area of the connection used for passing the raw GitHub 131 | response around during the callback. 132 | """ 133 | def handle_cleanup!(conn) do 134 | conn 135 | |> put_private(:github_user, nil) 136 | |> put_private(:github_token, nil) 137 | end 138 | 139 | @doc """ 140 | Fetches the `:uid` field from the GitHub response. 141 | 142 | This defaults to the option `:uid_field` which in-turn defaults to `:id` 143 | """ 144 | def uid(conn) do 145 | conn |> option(:uid_field) |> to_string() |> fetch_uid(conn) 146 | end 147 | 148 | @doc """ 149 | Includes the credentials from the GitHub response. 150 | """ 151 | def credentials(conn) do 152 | token = conn.private.github_token 153 | scope_string = token.other_params["scope"] || "" 154 | scopes = String.split(scope_string, ",") 155 | 156 | %Credentials{ 157 | token: token.access_token, 158 | refresh_token: token.refresh_token, 159 | expires_at: token.expires_at, 160 | token_type: token.token_type, 161 | expires: !!token.expires_at, 162 | scopes: scopes 163 | } 164 | end 165 | 166 | @doc """ 167 | Fetches the fields to populate the info section of the `Ueberauth.Auth` 168 | struct. 169 | """ 170 | def info(conn) do 171 | user = conn.private.github_user 172 | allow_private_emails = Keyword.get(options(conn), :allow_private_emails, false) 173 | 174 | %Info{ 175 | name: user["name"], 176 | description: user["bio"], 177 | nickname: user["login"], 178 | email: maybe_fetch_email(user, allow_private_emails), 179 | location: user["location"], 180 | image: user["avatar_url"], 181 | urls: %{ 182 | followers_url: user["followers_url"], 183 | avatar_url: user["avatar_url"], 184 | events_url: user["events_url"], 185 | starred_url: user["starred_url"], 186 | blog: user["blog"], 187 | subscriptions_url: user["subscriptions_url"], 188 | organizations_url: user["organizations_url"], 189 | gists_url: user["gists_url"], 190 | following_url: user["following_url"], 191 | api_url: user["url"], 192 | html_url: user["html_url"], 193 | received_events_url: user["received_events_url"], 194 | repos_url: user["repos_url"] 195 | } 196 | } 197 | end 198 | 199 | @doc """ 200 | Stores the raw information (including the token) obtained from the GitHub 201 | callback. 202 | """ 203 | def extra(conn) do 204 | %Extra{ 205 | raw_info: %{ 206 | token: conn.private.github_token, 207 | user: conn.private.github_user 208 | } 209 | } 210 | end 211 | 212 | defp fetch_uid("email", conn) do 213 | # private email will not be available as :email and must be fetched 214 | allow_private_emails = Keyword.get(options(conn), :allow_private_emails, false) 215 | fetch_email!(conn.private.github_user.user, allow_private_emails) 216 | end 217 | 218 | defp fetch_uid(field, conn) do 219 | conn.private.github_user[field] 220 | end 221 | 222 | defp fetch_email!(user, allow_private_emails) do 223 | maybe_fetch_email(user, allow_private_emails) || 224 | raise "Unable to access the user's email address" 225 | end 226 | 227 | defp maybe_fetch_email(user, allow_private_emails) do 228 | user["email"] || 229 | maybe_get_primary_email(user) || 230 | maybe_get_private_email(user, allow_private_emails) 231 | end 232 | 233 | defp maybe_get_primary_email(user) do 234 | if user["emails"] && Enum.count(user["emails"]) > 0 do 235 | Enum.find(user["emails"], & &1["primary"])["email"] 236 | end 237 | end 238 | 239 | defp maybe_get_private_email(user, allow_private_emails) do 240 | if allow_private_emails do 241 | "#{user["id"]}+#{user["login"]}@users.noreply.github.com" 242 | end 243 | end 244 | 245 | defp fetch_user(conn, token) do 246 | conn = put_private(conn, :github_token, token) 247 | # Will be better with Elixir 1.3 with/else 248 | case Ueberauth.Strategy.Github.OAuth.get(token, "/user") do 249 | {:ok, %OAuth2.Response{status_code: 401, body: _body}} -> 250 | set_errors!(conn, [error("token", "unauthorized")]) 251 | 252 | {:ok, %OAuth2.Response{status_code: status_code, body: user}} 253 | when status_code in 200..399 -> 254 | case Ueberauth.Strategy.Github.OAuth.get(token, "/user/emails") do 255 | {:ok, %OAuth2.Response{status_code: status_code, body: emails}} 256 | when status_code in 200..399 -> 257 | user = Map.put(user, "emails", emails) 258 | put_private(conn, :github_user, user) 259 | 260 | # Continue on as before 261 | {:error, _} -> 262 | put_private(conn, :github_user, user) 263 | end 264 | 265 | {:error, %OAuth2.Error{reason: reason}} -> 266 | set_errors!(conn, [error("OAuth2", reason)]) 267 | 268 | {:error, %OAuth2.Response{body: %{"message" => reason}}} -> 269 | set_errors!(conn, [error("OAuth2", reason)]) 270 | 271 | {:error, _} -> 272 | set_errors!(conn, [error("OAuth2", "unknown error")]) 273 | end 274 | end 275 | 276 | defp option(conn, key) do 277 | Keyword.get(options(conn), key, Keyword.get(default_options(), key)) 278 | end 279 | 280 | defp with_scopes(opts, conn) do 281 | scopes = conn.params["scope"] || option(conn, :default_scope) 282 | 283 | opts |> Keyword.put(:scope, scopes) 284 | end 285 | 286 | defp with_redirect_uri(opts, conn) do 287 | if option(conn, :send_redirect_uri) do 288 | opts |> Keyword.put(:redirect_uri, callback_url(conn)) 289 | else 290 | opts 291 | end 292 | end 293 | end 294 | -------------------------------------------------------------------------------- /lib/ueberauth/strategy/github/oauth.ex: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Github.OAuth do 2 | @moduledoc """ 3 | An implementation of OAuth2 for GitHub. 4 | 5 | To add your `:client_id` and `:client_secret` include these values in your 6 | configuration: 7 | 8 | config :ueberauth, Ueberauth.Strategy.Github.OAuth, 9 | client_id: System.get_env("GITHUB_CLIENT_ID"), 10 | client_secret: System.get_env("GITHUB_CLIENT_SECRET") 11 | 12 | """ 13 | use OAuth2.Strategy 14 | 15 | @defaults [ 16 | strategy: __MODULE__, 17 | site: "https://api.github.com", 18 | authorize_url: "https://github.com/login/oauth/authorize", 19 | token_url: "https://github.com/login/oauth/access_token", 20 | headers: [{"user-agent", "ueberauth-github"}] 21 | ] 22 | 23 | @doc """ 24 | Construct a client for requests to GitHub. 25 | 26 | Optionally include any OAuth2 options here to be merged with the defaults: 27 | 28 | Ueberauth.Strategy.Github.OAuth.client( 29 | redirect_uri: "http://localhost:4000/auth/github/callback" 30 | ) 31 | 32 | This will be setup automatically for you in `Ueberauth.Strategy.Github`. 33 | 34 | These options are only useful for usage outside the normal callback phase of 35 | Ueberauth. 36 | """ 37 | def client(opts \\ []) do 38 | config = 39 | :ueberauth 40 | |> Application.fetch_env!(Ueberauth.Strategy.Github.OAuth) 41 | |> check_credential(:client_id) 42 | |> check_credential(:client_secret) 43 | 44 | client_opts = 45 | @defaults 46 | |> Keyword.merge(config) 47 | |> Keyword.merge(opts) 48 | 49 | json_library = Ueberauth.json_library() 50 | 51 | client_opts 52 | |> OAuth2.Client.new() 53 | |> OAuth2.Client.put_serializer("application/json", json_library) 54 | end 55 | 56 | @doc """ 57 | Provides the authorize url for the request phase of Ueberauth. 58 | 59 | No need to call this usually. 60 | """ 61 | def authorize_url!(params \\ [], opts \\ []) do 62 | opts 63 | |> client 64 | |> OAuth2.Client.authorize_url!(params) 65 | end 66 | 67 | def get(token, url, headers \\ [], opts \\ []) do 68 | [token: token] 69 | |> client 70 | |> put_param("client_secret", client().client_secret) 71 | |> OAuth2.Client.get(url, headers, opts) 72 | end 73 | 74 | def get_token!(params \\ [], options \\ []) do 75 | headers = Keyword.get(options, :headers, []) 76 | options = Keyword.get(options, :options, []) 77 | client_options = Keyword.get(options, :client_options, []) 78 | client = OAuth2.Client.get_token!(client(client_options), params, headers, options) 79 | client.token 80 | end 81 | 82 | # Strategy Callbacks 83 | 84 | def authorize_url(client, params) do 85 | OAuth2.Strategy.AuthCode.authorize_url(client, params) 86 | end 87 | 88 | def get_token(client, params, headers) do 89 | client 90 | |> put_param("client_secret", client.client_secret) 91 | |> put_header("Accept", "application/json") 92 | |> OAuth2.Strategy.AuthCode.get_token(params, headers) 93 | end 94 | 95 | defp check_credential(config, key) do 96 | check_config_key_exists(config, key) 97 | 98 | case Keyword.get(config, key) do 99 | value when is_binary(value) -> 100 | config 101 | 102 | {:system, env_key} -> 103 | case System.get_env(env_key) do 104 | nil -> 105 | raise "#{inspect(env_key)} missing from environment, expected in config :ueberauth, Ueberauth.Strategy.Github" 106 | 107 | value -> 108 | Keyword.put(config, key, value) 109 | end 110 | end 111 | end 112 | 113 | defp check_config_key_exists(config, key) when is_list(config) do 114 | unless Keyword.has_key?(config, key) do 115 | raise "#{inspect(key)} missing from config :ueberauth, Ueberauth.Strategy.Github" 116 | end 117 | 118 | config 119 | end 120 | 121 | defp check_config_key_exists(_, _) do 122 | raise "Config :ueberauth, Ueberauth.Strategy.Github is not a keyword list, as expected" 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Github.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/ueberauth/ueberauth_github" 5 | @version "0.8.3" 6 | 7 | def project do 8 | [ 9 | app: :ueberauth_github, 10 | version: @version, 11 | name: "Üeberauth GitHub", 12 | elixir: "~> 1.3", 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | docs: docs(), 17 | package: package() 18 | ] 19 | end 20 | 21 | def application do 22 | [ 23 | applications: [:logger, :ueberauth, :oauth2] 24 | ] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:oauth2, "~> 1.0 or ~> 2.0"}, 30 | {:ueberauth, "~> 0.7"}, 31 | {:credo, "~> 0.8", only: [:dev, :test], runtime: false}, 32 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 33 | ] 34 | end 35 | 36 | defp docs do 37 | [ 38 | extras: [ 39 | "CHANGELOG.md": [title: "Changelog"], 40 | "CONTRIBUTING.md": [title: "Contributing"], 41 | LICENSE: [title: "License"], 42 | "README.md": [title: "Overview"] 43 | ], 44 | main: "readme", 45 | source_url: @source_url, 46 | source_ref: "#v{@version}", 47 | formatters: ["html"] 48 | ] 49 | end 50 | 51 | defp package do 52 | [ 53 | description: "An Ueberauth strategy for using Github to authenticate your users.", 54 | files: ["lib", "mix.exs", "README.md", "LICENSE"], 55 | maintainers: ["Daniel Neighman", "Sean Callan"], 56 | licenses: ["MIT"], 57 | links: %{ 58 | Changelog: "https://hexdocs.pm/ueberauth_github/changelog.html", 59 | GitHub: @source_url 60 | } 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, 4 | "credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "539596b6774069260d5938aa73042a2f5157e1c0215aa35f5a53d83889546d14"}, 5 | "earmark": {:hex, :earmark, "1.3.5", "0db71c8290b5bc81cb0101a2a507a76dca659513984d683119ee722828b424f6", [:mix], [], "hexpm", "762b999fd414fb41e297944228aa1de2cd4a3876a07f968c8b11d1e9a2190d07"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, 7 | "ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"}, 8 | "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "c2790c9f0f7205f4a362512192dee8179097394400e745e4d20bab7226a8eaad"}, 9 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, 10 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, 11 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 12 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 13 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 14 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 15 | "mime": {:hex, :mime, "2.0.1", "0de4c81303fe07806ebc2494d5321ce8fb4df106e34dd5f9d787b637ebadc256", [:mix], [], "hexpm", "7a86b920d2aedce5fb6280ac8261ac1a739ae6c1a1ad38f5eadf910063008942"}, 16 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 17 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 18 | "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"}, 19 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 20 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [: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", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, 21 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 22 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm", "603561dc0fd62f4f2ea9b890f4e20e1a0d388746d6e20557cafb1b16950de88c"}, 23 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 24 | "ueberauth": {:hex, :ueberauth, "0.10.3", "4a3bd7ab7b5d93d301d264f0f6858392654ee92171f4437d067d1ae227c051d9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "1394f36a6c64e97f2038cf95228e7e52b4cb75417962e30418fbe9902b30e6d3"}, 25 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, 26 | } 27 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/ueber_github_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UeberauthGithubTest do 2 | use ExUnit.Case 3 | doctest UeberauthGithub 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | --------------------------------------------------------------------------------