Subspace A private sphere in cyberspace.

Use OpenIDC with OpenResty

If one has many self-deployed and private services, a unified and painless authentication experience is very important. I have been searching for such solutions for a long time, and finally satisfied with OpenResty and lua-resty-openidc.

HTTP Basic Authentication is interrupting (the popup) and annoying. Worse, there is no solution to extend the authentication expiration time.

The services deployed for my lab use the SSO interface that provided by GitLab. Many other services, including statping and Seafile, use GitLab SSO as the login entry point. However, it would be funny to deploy such a heavy service just for its authentication functionality. Further, even if we have the SSO server, it still needs additional steps (hand-writing some glue codes) to connect the authentication server with other services.

By surfing the Internet, I decided to give openidc and dex-idp a try.

Dex

Dex implements OpenID Connect. It is related to two kinds of applications. The first would consume the authentication. In our use case, they are self-deployed private services. Another kind of applications will produce the auth, tell Dex the profile (or other information) of the user (if he/she is successfully authenticated), e.g., GitLab, GitHub, Google, and SAML servers. Of course, Dex can be used with simple Email-password pairs.

Dex official provides detailed docs of how to config the dex server.

If you are using Arch Linux like me, AUR also has a packaged named dex-idp (although it is abandoned). Refer to that, it is easy to write a usable PKGBUILD. Note that there is a typo in the provided file dex.service.

Example PKGBUILD of v2.28.1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pkgname='dex-idp'
pkgdesc='OpenID Connect Identity (OIDC) and OAuth 2.0 Provider with Pluggable Connectors'
pkgver='2.28.1'
pkgrel='1'
url="https://github.com/dexidp/dex"
license=('Apache')
arch=('x86_64')
makedepends=('go')
depends=('glibc')
backup=('etc/dex.yaml')
source=("git+https://github.com/dexidp/dex#tag=v${pkgver}" "dex.service" "dex.sysusers")
sha256sums=('SKIP'
            'SKIP'
            '610ae818f2ff08ac41f6beb227510bff5c55699041e94cbcfec44dfa5553e688')

prepare () {
  export GOPATH="${srcdir}"
  mkdir -p src/github.com/dexidp/
  mv "dex" "src/github.com/dexidp/dex"
  export PACKAGE_ROOT="${GOPATH}/src/github.com/dexidp/dex"
}

build () {
  cd "$PACKAGE_ROOT"
  make
}

package () {
  cd "$PACKAGE_ROOT"
  install -Dm755 bin/dex "${pkgdir}/usr/bin/dex" &&
  install -Dm644 examples/config-dev.yaml "${pkgdir}/etc/dex.yaml" &&
  install -Dm644 "${srcdir}/dex.sysusers" "${pkgdir}/usr/lib/sysusers.d/dex.conf" &&
  install -Dm644 "${srcdir}/dex.service" "${pkgdir}/usr/lib/systemd/system/dex.service" &&
  install -dm755 "${pkgdir}/usr/share/dex" &&
  cp -r web "${pkgdir}/usr/share/dex"
}

resty-openidc

The lua module openidc make it possible to use OpenID authentication in OpenResty web server. Following the official example, we can config the OpenID Connect Discovery, then set App Secrets and App ID.

It is quite flexible to handle the authentication results. For example, only allow the user with a specific Email to access the service.

Part of openresty configuration
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
location / {
    access_by_lua_block {
        local opts = {
            redirect_uri_path = "$REDIR_URI",
            discovery = "https://default-server/.well-known/openid-configuration",
            client_id = "$CLIENT_ID",
            client_secret = "$CLIENT_SECRET",
        }

        local res, err = require("resty.openidc").authenticate(opts)
        -- Failed to authenticate
        if err then
               ngx.status = 500
               ngx.say(err)
               ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
        end

        -- Authenticated, but no admin
        if res.user.email ~= "someone@mail.com" then
               ngx.status = 403
               ngx.exit(ngx.HTTP_FORBIDDEN)
        end

        ngx.req.set_header("X-USER", res.id_token.sub)
    }
}

The user will be redirected several times when he/she visits a private path to complete the autentication:

  1. Service -> Dex
  2. Dex -> Auth Server (GitHub/Google)
  3. Auth Server -> Dex
  4. Dex -> Service/503

If the browser session has been already authenticated, the progress will be imperceptible, which is very elegant.