Skip to content

Forward authentication

Forward authentication lets masks sign actors in to an app that has no sign-in of its own, such as a dashboard or an admin page.

  • The app sits behind a reverse proxy, a server that receives each request first and passes it on to the app.
  • The proxy asks masks about each request.
  • The proxy lets a request through only for an actor who is signed in to masks and may use the app.
  • The proxy passes the actor’s details to the app in headers.
  1. In /manage, open Clients and choose Add client.
  2. Under What kind of client, choose A known product.
  3. Under Product, choose Any app behind Caddy, Traefik, or nginx.
  4. Under Its address, enter the app’s address, such as https://dash.example.com.
  5. Choose Add the client.

masks registers the app and shows the proxy settings for that address. It also lists the app under Your apps on each person’s account page.

To limit the app to some groups, use Who may sign in on its Access tab, as for any other client.

  1. The proxy asks masks’ /forward-auth about each request. If the request carries a live cookie for the app, masks answers 200 with these headers, and the proxy hands them to the app:

    Header
    Remote-User The actor’s nickname, or their email address when they have no nickname.
    Remote-Email Their email address, when it is confirmed.
    Remote-Name Their name, when they have one.
    Remote-Groups The groups claim, separated by commas. It can be empty.
  2. If the request has no live cookie, masks sends the browser to /forward-auth/start on masks’ own address. masks signs the actor in there, unless they are already signed in.

  3. masks sends the browser back to /masks-auth/callback on the app’s address, with a code. The code works once, for that app only, for two minutes.

  4. The proxy hands that path to masks. masks sets a cookie for the app’s host alone and returns the browser to the page it asked for.

masks answers 403 when no app is registered for the address, or when a rule closes the path. An actor who is not in a group that Who may sign in allows gets a 403 page from masks after they sign in.

The cookie lasts as long as the actor’s masks session. Each app has its own cookie, so one app never sees another app’s cookie. Access to the app ends on the next request when the actor:

  • signs out of masks
  • ends the session from the account page
  • is suspended
  • is no longer in a group that Who may sign in allows

Rules let one app treat its paths differently. A manager can close /admin to everyone outside a group, leave /share open, let scripts in with a token, and skip the second factor on a home network.

To add rules, open the app’s Access tab in /manage and use Rules. Each rule covers the requests that match all of its fields. A field left empty covers everything.

Paths One per line. * covers one segment and ** covers any depth, so /admin/** covers /admin and everything under it.
Methods Such as GET and POST.
Networks Addresses or ranges, such as 192.168.0.0/16 or 2001:db8::/32.
Groups Group keys, or organization roles as key:role. A rule with groups waits until the person signs in, then covers only members.

Each rule has one Outcome:

Open to everyone Lets the request through with no sign-in. When the person is signed in, the app still gets their headers.
A signed-in person, without a second factor Asks the person to sign in, but does not ask for their second factor.
A signed-in person, with their second factor Asks for the second factor of a person who has one, even when they signed in without it.
A signed-in person, or a script with an access token Also lets a script through when it sends Authorization: Bearer with an access token for the app. See scripts.
Closed to everyone Answers 403.

masks reads the rules from the top. The first rule that covers a request decides it. A request no rule covers needs a signed-in person, as it does with no rules at all. Use Move up and Move down to change the order.

Who may sign in still applies to every rule except Open to everyone. A rule cannot let in a person outside the groups Who may sign in allows.

These rules close the admin pages, open shared links, and skip the second factor at home:

  1. Paths /admin/**, Groups admins, Outcome a signed-in person, with their second factor.
  2. Paths /admin/**, Outcome closed to everyone.
  3. Paths /share/**, Outcome open to everyone.
  4. Networks 192.168.0.0/16, Outcome a signed-in person, without a second factor.

Everything else needs a signed-in person, with their second factor when they have one.

A method rule needs the proxy to send the request’s method. Caddy and Traefik send X-Forwarded-Method on their own. In nginx, add this line to the /masks-check location:

proxy_set_header X-Original-Method $request_method;

A network rule reads the address of the person’s browser from X-Forwarded-For. masks reads that header only from a proxy listed in MASKS_TRUSTED_PROXIES, or from a private address. From any other address, masks uses the address of the connection itself.

A script signs in as its own client with the client_credentials grant and asks for a token for the app’s address as its resource. See signing in as itself. The script’s client needs the app’s address, such as https://dash.example.com, in its Resources.

Terminal window
curl -H "Authorization: Bearer $TOKEN" https://dash.example.com/api/items

masks lets the request through when the token is live and was issued for the app. The app gets the script’s client id in Remote-User and its name in Remote-Name.

dash.example.com {
forward_auth https://auth.example.com {
uri /forward-auth
header_up Host auth.example.com
copy_headers Remote-User Remote-Email Remote-Name Remote-Groups
}
reverse_proxy dash:8080
}

Keep header_up Host. Without it, Caddy sends the app’s host to masks.

http:
middlewares:
masks:
forwardAuth:
address: https://auth.example.com/forward-auth
authResponseHeaders:
[Remote-User, Remote-Email, Remote-Name, Remote-Groups]

Add the masks middleware to the app’s router.

nginx’s auth_request cannot follow a redirect. For that reason, the nginx setup differs in two ways:

  • It asks masks for a 401 and sends the browser to sign in itself.
  • It hands the callback to masks with a location of its own.
location = /masks-check {
internal;
proxy_pass https://auth.example.com/forward-auth?status=401;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_ssl_server_name on;
}
location /masks-auth/ {
proxy_pass https://auth.example.com/forward-auth/;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
}
location / {
auth_request /masks-check;
auth_request_set $remote_user $upstream_http_remote_user;
proxy_set_header Remote-User $remote_user;
error_page 401 =302 https://auth.example.com/forward-auth/start?rd=$scheme://$http_host$request_uri;
proxy_pass http://dash:8080;
}

To pass Remote-Email, Remote-Name, or Remote-Groups too, add an auth_request_set and a proxy_set_header line for each one, as for Remote-User.

A request that carries an Authorization: Basic header is answered from that header alone, with no cookie and no redirect. The name is the person’s nickname or email address, and the password is an app password made for this client. masks answers 200 with the same headers as a browser sign-in, or 401.

Terminal window
curl -u ada:xxxx-xxxx-xxxx-xxxx-xxxx-xxxx https://dash.example.com/reports

The account password is never accepted here, and neither is an app password made for another client. The client’s Accept app passwords has to be on. A wrong app password counts against the account’s sign-in limits, and masks answers 429 once they are reached.

The app can trust Remote-User only because the proxy sets it. Set up two things:

  1. Make sure nothing reaches the app except through the proxy.
  2. Make the proxy drop any Remote- header a browser sends, before it adds the headers masks answered with.

A nickname that an actor gives up is held for 30 days, so a new account cannot become the old Remote-User right away. See accounts.