OAuth
Introduction
Next to normal email login, IHP also provides support for OAuth-login with third-party identity providers. Login via OAuth requires IHP Pro.
At the moment IHP supports these identity providers:
- GitHub
Login with Google
Setup
To use the Login with Google functionality you first need to enable the ihp-oauth-google
package.
Open your project’s flake.nix
and a ihp-oauth-google
dependency to haskellDeps
:
let
ihp = ..
haskellEnv = import "${ihp}/NixSupport/default.nix" {
ihp = ihp;
haskellDeps = p: with p; [
# ...
ihp-oauth-google # <----- ADD THIS LINE
];
otherDeps = p: with p; [
];
projectPath = ./.;
};
in
haskellEnv
After that stop your local development server and run the following command to install the package:
devenv up
Schema Changes
For Google OAuth Login to work, we need to add a google_user_id TEXT
column to our users
table. The column needs to be nullable and have NULL
as the default value.
Open Application/Schema.sql
and add google_user_id TEXT
to the CREATE TABLE users
statement:
CREATE TABLE users (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
-- ... ,
google_user_id TEXT
);
Creating the Controller
Next we need to add the GoogleOAuth
controller. For that create a new file Web/Controller/GoogleOAuth.hs
with the following content:
module Web.Controller.GoogleOAuth where
import Web.Controller.Prelude
import Web.Controller.Sessions ()
import IHP.OAuth.Google.Controller
import qualified IHP.OAuth.Google.Types as Google
instance Controller Google.GoogleOAuthController where
action Google.NewSessionWithGoogleAction = newSessionWithGoogleAction @User
action Google.GoogleConnectCallbackAction = googleConnectCallbackAction @User
instance GoogleOAuthControllerConfig User where
beforeCreateUser user googleClaims = user |> set #isConfirmed True -- This will auto-confirm your user's email
Routing
We also need to enable routing for this controller. Open Web/Routes.hs
and
-
Import the type definition by adding this import to the top of the file:
import IHP.OAuth.Google.Types
-
Enable AutoRoute by adding this to end of the file:
instance AutoRoute GoogleOAuthController
FrontController
Next we need to enable this controller inside the FrontController, so that requests can be routed there.
Open Web/FrontController.hs
and
-
Add the following imports to the top of the file:
import IHP.OAuth.Google.Types import Web.Controller.GoogleOAuth
-
Add
GoogleOAuthController
to the list of controllers:instance FrontController WebApplication where controllers = [ startPage StartpageAction -- ... , parseRoute @GoogleOAuthController -- <----- ADD THIS ]
View Prelude
As the types for the controller are defined in IHP.OAuth.Google.Types
instead of the usual Web.Types
we need to make changes to our Web/View/Prelude.hs
:
module Web.View.Prelude
( module IHP.ViewPrelude
-- ...
, module IHP.OAuth.Google.Types -- <---- ADD THIS
) where
import IHP.ViewPrelude
-- ...
import IHP.OAuth.Google.Types -- <---- ADD THIS
This ensures that we can write pathTo NewSessionWithGoogleAction
and similiar calls without always manually needing to add import statements.
Config
Before the controller can be used, we need to configure the google client id. A client id looks like this: 1234567890-abc123def456.apps.googleusercontent.com
.
You can create a google client id by logging into the Google API console and adding a new project. Additionally you need to enable and configure OAuth inside the API console. Follow this Guide by Google if you haven’t done this before.
Once you got your client id, you can continue here:
-
Open
Config/Config.hs
-
Add this import:
import IHP.OAuth.Google.Config
-
Call
initGoogleOAuth
:config :: ConfigBuilder config = do option Development option (AppHostname "localhost") initGoogleOAuth -- <--- ADD THIS
The call to initGoogleOAuth
will read the OAUTH_GOOGLE_CLIENT_ID
environment variable and pass it to the GoogleOAuthController
. We also need to define this environment variable locally. For that open the start
script in your project and add a OAUTH_GOOGLE_CLIENT
statement like this:
# ...
export OAUTH_GOOGLE_CLIENT_ID="MY_GOOGLE_CLIENT_ID" # <-- ADD THIS LINE BEFORE THE RunDevServer CALL
RunDevServer
You need to replace the MY_GOOGLE_CLIENT_ID
with your google client id. After making the changes to start
, you need to restart your local dev server.
Trying it out
Now that Google OAuth is configured, we can already test it out.
Open http://localhost:8000/NewSessionWithGoogle
and click on the Login with Google
button. It should open a google login dialog and then continue logging into the app.
If google shows you an error message, check the JavaScript error console for details. A common error is that the localhost:8000
host is not on the host whiteliste of the Google Client ID.
Optional: Adding the Login with Google Button
Typically you don’t want to redirect your users to http://localhost:8000/NewSessionWithGoogle
, instead you want to place a Login with Google
button on your existing login page.
Open Web/Sessions/New.hs
and apply these changes:
module Web.View.Sessions.New where
import Web.View.Prelude
import IHP.AuthSupport.View.Sessions.New
instance View (NewView User) where
html NewView { .. } = [hsx|
<div class="h-100" id="sessions-new">
<div class="d-flex align-items-center">
<div class="w-100">
<div style="max-width: 400px" class="mx-auto mb-5">
<h5>Please login</h5>
{renderForm user}
{loginWithGoogle} <!-- ADD THIS CALL HERE -->
</div>
</div>
</div>
</div>
|]
-- ...
loginWithGoogle :: Html
loginWithGoogle = [hsx|
<div id="g_id_onload"
data-client_id={googleClientId}
data-context="signin"
data-ux_mode="popup"
data-callback="onGoogleLogin"
data-auto_prompt="false"
>
</div>
<div class="g_id_signin"
data-type="standard"
data-shape="rectangular"
data-theme="outline"
data-text="continue_with"
data-size="large"
data-logo_alignment="left"
data-width="304">
</div>
<form method="POST" action={GoogleConnectCallbackAction} id="new-session-with-google-form">
<input type="hidden" name="jwt" value=""/>
</form>
<script src="/google-login.js?v2"></script>
<script src="https://accounts.google.com/gsi/client"></script>
|]
where
googleClientId :: Text = "YOUR GOOGLE CLIENT ID"
The login process uses the Google Login JS SDK and requires a little bit of JavaScript to work. Create a file static/google-login.js
with this content:
function onGoogleLogin(response) {
var form = document.getElementById('new-session-with-google-form');
form.querySelector('input[name="jwt"]').value = response.credential;
form.submit();
}
Now you should be able to log in with Google from your normal login page.
Login with GitHub
Setup
To use the Login with GitHub functionality you first need to enable the ihp-oauth-github
package.
Open your project’s default.nix
and a ihp-oauth-github
dependency to haskellDeps
:
let
ihp = ..
haskellEnv = import "${ihp}/NixSupport/default.nix" {
ihp = ihp;
haskellDeps = p: with p; [
# ...
ihp-oauth-github # <----- ADD THIS LINE
];
otherDeps = p: with p; [
];
projectPath = ./.;
};
in
haskellEnv
After that stop your local development server and run the following command to install the package:
devenv up
Schema Changes
For GitHub OAuth Login to work, we need to add a github_user_id INT
column to our users
table. The column needs to be nullable and have NULL
as the default value.
Open Application/Schema.sql
and add github_iser_id INT
to the CREATE TABLE users
statement:
CREATE TABLE users (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
-- ... ,
github_user_id INT DEFAULT NULL UNIQUE,
);
Creating the Controller
Next we need to add the GithubOAuth
controller. For that create a new file Web/Controller/GithubOAuth.hs
with the following content:
module Web.Controller.GithubOAuth where
import Web.Controller.Prelude
import Web.Controller.Sessions ()
import IHP.OAuth.Github.Controller
import qualified IHP.OAuth.Github.Types as Github
instance Controller Github.GithubOAuthController where
action Github.NewSessionWithGithubAction = newSessionWithGithubAction @User
action Github.GithubConnectCallbackAction = githubConnectCallbackAction @User
instance GithubOAuthControllerConfig User where
beforeCreateUser user githubUser = user |> set #isConfirmed True -- This will auto-confirm your user's email. Remove this line if you don't use email confirmation
afterCreateUser user = do
-- Called after a new user is created
-- E.g. you could send a welcome email here:
--
-- > sendMail WelcomeMail { .. }
--
pure ()
beforeLogin user githubUser = do
-- Called before the user is logged in
-- Here you can e.g. set the app's profile picture to the one provided by github:
--
-- > user
-- > |> setJust #profilePicture githubUser.avatarUrl
-- > |> setJust #githubName githubUser.login
-- > |> pure
--
pure user
Routing
We also need to enable routing for this controller. Open Web/Routes.hs
and
-
Import the type definition by adding this import to the top of the file:
import IHP.OAuth.Github.Types
-
Enable AutoRoute by adding this to end of the file:
instance AutoRoute GithubOAuthController
FrontController
Next we need to enable this controller inside the FrontController, so that requests can be routed there.
Open Web/FrontController.hs
and
-
Add the following imports to the top of the file:
import IHP.OAuth.Github.Types import Web.Controller.GithubOAuth
-
Add
GithubOAuthController
to the list of controllers:instance FrontController WebApplication where controllers = [ startPage StartpageAction -- ... , parseRoute @GithubOAuthController -- <----- ADD THIS ]
View Prelude
As the types for the controller are defined in IHP.OAuth.Github.Types
instead of the usual Web.Types
we need to make changes to our Web/View/Prelude.hs
:
module Web.View.Prelude
( module IHP.ViewPrelude
-- ...
, module IHP.OAuth.Github.Types -- <---- ADD THIS
) where
import IHP.ViewPrelude
-- ...
import IHP.OAuth.Github.Types -- <---- ADD THIS
This ensures that we can write pathTo NewSessionWithGithubAction
and similiar calls without always manually needing to add import statements.
Config
Before the controller can be used, we need to configure the github client id and client secret.
Follow this Guide by GitHub to create a GitHub app and get your client id and secret.. During the setup you need to provide a Callback URL. Set this to http://localhost:8000/GithubConnectCallback
.
Once you got your client id and secret, you can continue here:
-
Open
Config/Config.hs
-
Add this import:
import IHP.OAuth.Github.Config
-
Call
initGithubOAuth
:config :: ConfigBuilder config = do option Development option (AppHostname "localhost") initGithubOAuth -- <--- ADD THIS
The call to initGithubOAuth
will read the OAUTH_GITHUB_CLIENT_ID
and OAUTH_GITHUB_CLIENT_SECRET
environment variables, and pass it to the GithubOAuthController
. We also need to define this environment variable locally. For that open the start
script in your project and add two statements like this:
# ...
export OAUTH_GITHUB_CLIENT_ID="MY_GITHUB_CLIENT_ID" # <-- ADD THIS LINE BEFORE THE RunDevServer CALL
export OAUTH_GITHUB_CLIENT_SECRET="MY_GITHUB_SECRET"
RunDevServer
You need to replace MY_GITHUB_CLIENT_ID
and MY_GITHUB_SECRET
with your values. After making the changes to start
, you need to restart your local dev server.
Trying it out
Now Github Login is ready and we can try it out.
Open http://localhost:8000/NewSessionWithGithub
and click on the Login with Github
button. It will redirect to github and then continue logging into the app.
You can place a Login with Github
button by linking to the NewSessionWithGithubAction
:
<a href={NewSessionWithGithubAction} target="_self" class="btn btn-primary">
Continue with GitHub
</a>
Use target="_self"
for these links to avoid turbolinks interfering with the redirect to github.
Now your github integration is ready to use.