Protect a web resource served by Apache httpd without making users "sign up" or add a new login and password combination to keep track of.
Make the login easy to use, minimally intrusive, and professional appearing.
mod_auth_openidc allows OpenID Connect (based on OAuth 2.0) authentication and authorization integration with Apache's built-in "Require"-type directives for resource protection.
This document assumes you have access to configure the Apache httpd instance from which your resources are served. It may be possible to complete the necessary configuration with .htaccess
files if you have adequate permissions and the server is configured properly otherwise, but we'll write these instructions for modifying a root (or included) configuration file. This document won't detail accessing other Google services beyond the login, though doing so is a relatively easy extension of the method given here. Additionally, there's no description of how to compile or install mod_auth_openidc on your server, though some instructions that may be applicable to select circumstances are available at mod_auth_openidc on a seedbox. Finally, there's no guarantee that this is the "easiest" or "best" method for achieving the above goals, but rather it's one that works for the author.
Finally, a warning about this document: to be as language-neutral and readable as possible, all example code is written to be in an Apache configuration file language, plain HTML, plain JavaScript, or shell script, all without attention to any specific optimizations. This in no way means yours needs to be implemented the identically.
The app considered in these instructions uses a typical frontend/backend configuration with an HTML+JavaScript "page" served without protected access (i.e., that is publicly available) and a server-side script-based "API" that is only accessible to authorized users. For the sake of this documentation, we'll refer to the root URL (https://www.example.com/) as the URL for the page, served from the directory /html
, and call the URL for the API https://www.example.com/api, served from the directory /html/api
.
-
Install mod_auth_openidc; we'll assume
mod_auth_openidc.so
is in/usr/bin/apache2/modules
. -
Create a path for the OAuth redirect (which we'll also use for other functions); let's create the
/oauth
directory, to be served from https://www.example.com/oauth and add the/oauth/redirect
directory specifically for the redirect. -
Enable Google authentication with mod_auth_openidc as instructed in the README:
OIDCProviderMetadataURL https://accounts.google.com/.well-known/openid-configuration OIDCClientID <your-client-id-administered-through-the-google-api-console> OIDCClientSecret <your-client-secret-administered-through-the-google-api-console> OIDCRedirectURI https://www.example.com/oauth/redirect OIDCCryptoPassphrase <password> OIDCScope "openid https://www.googleapis.com/auth/userinfo.email"
-
Add the directories to be protected:
<Directory "/oauth"> AuthName "Google Account Required" AuthType openid-connect Require valid-user </Directory> <Directory "/html/api"> AuthName "Google Account Required" AuthType openid-connect Require valid-user </Directory>
-
That's it for the primary goal. https://www.example.com/api now requires a Google login for access.
There are (at least) a couple of problems with this simple solution.
- The "flow" for a needed login is not as straightforward for the user as it might be. If the
https://www.example.com/api
destination is only to be used by a script, the user still has to authenticate somewhere first. We could protect the entire/html
directory with mod_auth_openidc, but then users would not get the "logged out" version of the site and instead be redirected straight to a Google signin page without other information. - Any Google user can access the
/html/api
directory; that's not very different from being public. - This login flow results in an access token and an id token being stored by mod_oauth_openidc, and a mod_oauth_openidc session length that's the same as the amount of time before the ID expires. For Google, ID tokens usually expire in 1 hour, so this means users have to log in again if it's been more than an hour since the last time they did.
Developing a more "user friendly" login process requires minimal extra development. Instead of protecting all of the /html
directory with mod_auth_openidc and throw them to a Google login screen without warning, we can add an API function that redirects the browser.
Page:
<a href="#" onclick="login();">Login with Google</a>
JavaScript:
function login() {
window.location.href="https://www.example.com/api?login"
}
in /html/api/index.sh
:
if [ "$QUERY_STRING" = "login" ]; then
cat <<-finished
Status: 303
Location: https://www.example.com/
Content-type: text/html
<!DOCTYPE html><html lang="en">
<head>
<meta charset="utf-8">
<title>Return to Sender'</title>
</head>
<body>
<h1>Return</h1>
<p>You have successfully logged in. You may
<a href="https://www.example.com/">return to the page
from whence you came</a></p>
</body>
</html>
finished
fi
Note that the /api
page is served after the authentication flow redirects back to it; it then redirects the browser to the now "logged in" page.
The next problem can be solved by ensuring the user logging in meets criteria other than valid-user
, which here is the same as "has a Google account". The mod_auth_openidc wiki page on Authorization covers several options for ensuring you're limiting access to the users you want. Perhaps the easiest is shown in the README: if your domain is associated with a Google Workspace and you'd just like to limit users to those in your domain, it suffices to replace Require valid-user
with Require claim hd:example.com
for the directories to be protected.
If you want to make it easier (or just appear more professional) for your users, add the mod_auth_openidc configuration line:
OIDCAuthRequestParams "hd=example.com"
This will make the usual Google login prompt instead display only those accounts associated with the example.com domain.
As noted above, this configuration will require users to "re-authenticate" by going through the Google login process every hour, potentially even in the middle of using the app, which can then also result in incomplete processing if an app function makes more than one request. This is due to mod_auth_openidc's session length; by default, this is set to the expiration time of Google's ID token, which is usually one hour. To increase the mod_auth_openidc session length, add the directives
OIDCSessionMaxDuration <seconds>
OIDCSessionInactivityTimeout <seconds>
As you can probably guess, the first is the session length from the time of initial login, and the second is the amount of time since the last request for the protected resource. The first defaults to the expiration time of an ID token, and the second defaults to 5 minutes. I've set them to
OIDCSessionMaxDuration 1206000
OIDCSessionInactivityTimeout 1206000
1,206,000 seconds is one hour less than 2 weeks. Google's sessions during which it considers a user login valid default to 2 weeks, so this ensures we don't try to access it beyond when Google will force a login, even though that would probably work seamlessly. By setting the OIDCSessionInactivityTimeout
to the same number, we effective disable session expiration based on inactivity alone.
Additionally, mod_auth_openidc accesses its sessions using a browser cookie. By default this is a "session cookie"; not to be confused with the Google session or the mod_auth_openidc session, this refers to the browser session; when the browser is closed or quit, the cookie is gone, and the user must again login. To make the cookie last at least as long as the mod_auth_openidc session, even if the browser is restarted, add the directive
OIDCSessionType server-cache:persistent
Finally, the mod_auth_openidc session will end when the Apache server (or other processes running it, like the operating system) is restarted. This isn't a problem for most people as that's relatively infrequent. However, if Apache (or server) restarts are frequent enough that the login frequency is bothersome, you can persist the mod_auth_openidc cache outside browser memory as noted in the mod_auth_openidc Caching wiki entry. The easiest method is to use "file" based caching, and to ensure you use a directory to which the Apache httpd process has write access. (By default, the directory is in a system temp directory, which is usually fine.) To switch to file-based caching, use
OIDCCacheType file
If you don't need the provided access token (which expires an hour after login) for any other reason, and you're satisfied by the security of the timeouts set above, you're done. mod_auth_openidc will authenticate the user at the time of login and assume that authentication is valid until one of the timeouts is met.
If the app uses a Google access token to access other Google services (say, to edit a file on Google Drive), it will only be able to do so for an hour before the access token expires. Similarly, if the backend security of the app requires ensuring the user is valid throughout the mod_auth_openidc session instead of just at the beginning of it, the ID token will only be usable for an hour before it expires. Obtaining new access and id tokens during the mod_auth_openidc session without interrupting the user every hour requires a bit more work.
mod_auth_openidc has excellent support for using refresh tokens and automatically obtaining new access tokens and user info when needed during the mod_auth_openidc session. It's even able to do so while the user and the app are inactive, thanks to the OIDCUserInfoRefreshInterval
directive. However, a refresh token can only be obtained from the Google login flow when showing the "consent" screen to the user, the "second page" that users see when "authorizing" an application to use their Google data the first time they want to do so. Since mod_auth_openidc associates the refresh token with a mod_auth_openidc session, this consent screen must be shown every time that session expires and the user needs to log in again. It is unexpected (and sometimes concerning) to users that they would have to keep "re-authorizing" your app, so it's a suboptimal solution.
Thanks to a discussion with Hans Zandbelt, mod_auth_openidc's creator, there's a solution which doesn't require a refresh token, and therefore doesn't require repeated consent screens. Google will re-authorize a user (and replace expired access and ID tokens) without user interaction as long as the associated Google session is still active. (By default, this is for 14 days or until a significant authorization change occurs, such as when a user revokes access or changes their password.) This "silent roundtrip authentication" uses the same flow as the usual login, but does not require the user to type or click anything. To the user, this is identical to the above method simply accepting the initial login as adequate authentication and authorization, but for the app it maintains an active access token and repeated reassurance that the user remains valid and has not revoked access permissions.
It may be possible to configure mod_auth_openidc to use "silent authentication" within the current session by (ab)using its methods for "step-up" authentication, but this still requires checking whether the tokens are expired,