Skip to content

Instantly share code, notes, and snippets.

@therealchjones
Last active January 9, 2024 16:30
Show Gist options
  • Save therealchjones/1c8db3a99b5373f1fce4c3eb79222009 to your computer and use it in GitHub Desktop.
Save therealchjones/1c8db3a99b5373f1fce4c3eb79222009 to your computer and use it in GitHub Desktop.
Easy Google authentication for Apache

Google authentication for Apache

What do we want?

Primary goal

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.

Secondary goal

Make the login easy to use, minimally intrusive, and professional appearing.

Potential solution

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.

Caveats

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.

Web app structure

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.

Basic server configuration

  1. Install mod_auth_openidc; we'll assume mod_auth_openidc.so is in /usr/bin/apache2/modules.

  2. 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.

  3. 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"
    
  4. 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>
  5. That's it for the primary goal. https://www.example.com/api now requires a Google login for access.

Improvements

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.

Login page

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.

Claims

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.

Session expiration

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.

Access & ID tokens

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.

Refresh tokens

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.

Silent reauthentication

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,

See also

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment