A developer-facing SaaS application benefits from a one-click sign in path next to the regular email and password form. Most of my users already have a GitHub account, so adding "Sign in with GitHub" removes one step from their first session without changing the user record the rest of the app already understands.
In this tutorial I install the oauth feature, register a GitHub OAuth
application on github.com, set the client ID and secret, and sign in
through the new link. The starting point is a project with authentication
already installed, since the OAuth flow ends in the same User row.
Step 1 - Install the oauth feature
First step is to run the installer.
mix saaskit.feature.install oauth
mix deps.get
mix ecto.migrate
The feature has no declared feature dependencies, but it only really makes
sense alongside authentication. Two packages land in mix.exs.
# mix.exs
{:ueberauth, "~> 0.10.8"},
{:ueberauth_github, "~> 0.8.3"},
The migration creates a new user_identities table. The reason for a
separate table is that one user may eventually connect more than one
provider, so the users schema is kept clean and the identity rows hang
off it through a belongs_to :user.
user_identities
provider :string
uid :string
user_id references(users) on_delete: :delete_all
unique_index(:user_identities, [:uid, :provider])
Step 2 - Register a GitHub OAuth application
Next step is to create the OAuth application on GitHub.
I open https://github.com/settings/developers, choose "New OAuth App",
and fill in the form.
- Homepage URL:
http://localhost:4000 - Authorization callback URL:
http://localhost:4000/auth/github/callback
GitHub then shows a client ID and lets me generate a client secret. I copy both.
Step 3 - Set the env vars
The feature reads the credentials from environment variables at compile time. So I export them in my shell before starting the server.
export GITHUB_CLIENT_ID=...your client id...
export GITHUB_CLIENT_SECRET=...your client secret...
The injected config in config/config.exs pulls them through and wires
the Ueberauth strategy.
# config/config.exs
config :ueberauth, Ueberauth,
providers: [
github: {Ueberauth.Strategy.Github, [default_scope: "user:email"]}
]
config :ueberauth, Ueberauth.Strategy.Github.OAuth,
client_id: System.get_env("GITHUB_CLIENT_ID"),
client_secret: System.get_env("GITHUB_CLIENT_SECRET")
Note that default_scope: "user:email" asks GitHub for the user's primary
email address. Without that scope, the callback would not be able to
match or create a User by email.
Step 4 - Look at the new routes
The installer injects an /auth scope into the router.
# lib/my_app_web/router.ex
scope "/auth", MyAppWeb do
pipe_through :browser
get "/:provider", OauthCallbackController, :request
get "/:provider/callback", OauthCallbackController, :callback
end
GET /auth/github is handled by Ueberauth's plug, which builds the
GitHub authorisation URL and redirects. GET /auth/github/callback is
handled by MyAppWeb.OauthCallbackController.callback/2, which receives
the auth result and either matches an existing User by email or creates
a new one with a random password.
The "Continue with GitHub" link is injected into both the login and registration LiveViews.
<.link href={~p"/auth/github"} class="btn btn-block">
Sign in with Github
</.link>
Step 5 - Sign in through GitHub
With these changes in place, I start the server and visit
/users/log_in.
mix phx.server
I click "Sign in with Github", approve the OAuth prompt on github.com,
and land back on the application as a signed-in user. If my GitHub email
already exists in users, the callback signs me into that account and
adds a user_identities row that links the two. If the email is new,
UserIdentities.find_or_create_user/1 creates a User with a random
password and returns {:just_created, user}, which the controller logs
in via UserAuth.log_in_user/2.
So either way, the rest of the app sees the same current_scope.user it
already understood, with no special handling for OAuth users elsewhere.
Final Result
I now have a second sign-in path next to the email and password form. The
underlying User record is the same one the rest of the app uses, which
means features like teams, admin, and notifications keep working without
any extra wiring. A natural next step is to install fun_with_flags so I
can roll changes out to a subset of these users by team or by identity.