Files
remark42/site/content/docs/configuration/authorization/index.md
T
Dmitry VerkhoturovandGitHub 4c9ef37cf1 Move the site from eleventy to hugo (#2179)
* Move the site from eleventy to hugo

The site is built by a single static binary. No node, no package manager,
no lockfile, and the toolchain it needed is gone: eleventy, tailwind,
postcss, markdown-it and its three plugins, date-fns, prism, npm-run-all,
cross-env and html-minifier-terser.

Hugo covers most of that itself. Chroma replaces prism, goldmark replaces
markdown-it, `--minify` replaces html-minifier-terser, and fingerprinted
asset URLs replace the cache-busting `version` shortcode that stamped
`Date.now()` into every stylesheet link.

`assets/styles.css` is hand-written, since tailwind was the only reason
left to keep a package manager. The palette and the light and dark values
are custom properties at the top of the file; the minified stylesheet is
15 kB against tailwind's 46 kB, and the whole build 1.0 MB against 1.2 MB.
It was matched to the old one by comparing computed styles rather than by
eye, which is how the heading weights and line heights, the list marker
colour, and the home page heading and sign-off were caught: the last of
those had been carried by tailwind utilities written into the markup.

The `::: note` container becomes a `note` shortcode taking the emoji to
show. Its closer needs a blank line after it, because a shortcode is not
a block rule the way `markdown-it-container` was, and without one goldmark
keeps the callout inside the open paragraph. The `overflow-x` wrapper
around tables and the heading anchors are goldmark render hooks.

Syntax guessing is off. Chroma detected a systemd unit file as gdscript
and a chat transcript as mysql, and colouring a snippet as the wrong
language is worse than not colouring it. The two chroma themes are scoped
to opposite sides of the theme switch rather than layered, because they do
not declare the same properties on the same tokens: github gives Error a
background github-dark never overrides, and styles Punctuation where
github-dark leaves it alone. Layered, either leaves a light value applying
on a dark page.

`[frontmatter] lastmod` resolves through git, then front matter, then file
modification time. Without that chain `.Lastmod` falls back to `.Date`,
which is zero when a page carries no date, and every page reads
`Jan 01, 0001`. `enableGitInfo` is off because the image build context is
`site/` alone, where hugo fails hard rather than degrading;
`HUGO_ENABLEGITINFO=true` gives real per-page commit dates locally.

Three fixes fall out of the move rather than being sought:

- `/docs/` redirected nowhere. The stub was a markdown file whose
  permalink was a template expression while `markdownTemplateEngine` was
  false, so it never rendered and the URL 404'd. It is an alias now
- `/docs/contributing/` pointed at `/docs/contributing/development/`,
  which has never existed. It points at the backend page
- the 404 page was built to `/404/` and nothing served it. Hugo writes it
  to `/404.html` and reproxy is told to use it

The mobile documentation menu is a checkbox and label. `visibility: hidden`
on the checkbox, which is what the old `invisible` utility set, takes it
out of the tab order, and a label is not focusable on its own, so the menu
could not be opened from the keyboard at all. The checkbox is clipped
rather than hidden, and its label shows a focus ring.

Content is unchanged. Every code block on every page is byte-identical to
the eleventy output; the only prose difference is that two example values,
`mysite.com` and a quoted `https://demo.remark42.com`, are no longer
turned into links, goldmark's linkify being narrower than markdown-it's.

`backend/README.md` and `frontend/apps/remark42/README.md` are symlinks
into the docs tree and follow it to `site/content/`, as does the path
`release.yml` watches. `frontend/CLAUDE.md` described the site as a node
and yarn project in four places.

* Keep the heading anchors markdown-it generated

Goldmark strips punctuation markdown-it kept, so 22 headings holding a
dot, slash, apostrophe, question mark, bracket or em dash would take a new
id and any link into one from outside the repository would stop resolving.

Those headings carry their previous id as well, as an empty target emitted
ahead of the heading by the render hook, from a map of content path to old
anchor in `data/anchor_aliases.json`. The map was built by matching
heading text between the two builds rather than by position, so it
survives a heading being added or moved.

The hook rather than markdown, because goldmark's `{#id}` attribute syntax
cannot express these: it accepts dots, apostrophes and em dashes but
treats a slash, a question mark, a bracket or a percent sign as heading
text, which is 11 of the 22. The ids are stored percent-decoded, since a
browser decodes a fragment before matching, so `#children%E2%80%99s-privacy`
finds `children’s-privacy`. Verified by navigating to the awkward ones
against the built image and measuring where the page settles: each lands
112px down, which is the header offset the target carries.

Three pages carried no title, so the docs template rendered an empty `<h1>`
above the heading their markdown already had. They take their titles from
that heading text, so neither the wording nor its anchor changes, and the
template's `<h1>` carries an id. One in-page link pointed at an anchor
goldmark no longer generates.

The heading render hook emits no permalink anchor. The one it replaced was
an empty `<a href>` with `pointer-events: none`, so it could not be
clicked, and its only job was a `::before` spacer that `scroll-margin-top`
on the heading already does. Being an `<a href>` it stayed in the tab
order, so every heading was an unexplained keyboard stop: eight on the
installation page alone. Fragment navigation still lands 112px down, clear
of the fixed header.

* Harden the site image build and its CI

The architecture guard could not fire. `${TARGETARCH:-amd64}` defaulted
before the `unsupported arch` branch was reachable, so a build without
buildkit put an amd64 hugo inside an aarch64 image and ran only because
Docker Desktop emulates it. Reproduced with `--build-arg TARGETARCH=`:
`/etc/apk/arch` reported aarch64 and `hugo version` linux/amd64. An empty
value is an error now. `Dockerfile.dev` had the same defect and no smoke
step to catch it, so it would have failed at `compose up`.

The hugo tarball is verified against the release's own `checksums.txt`,
and the match is asserted present before it is used: piping grep straight
into `sha256sum -c` left the guarantee resting on what the checker does
with empty input. Busybox exits 1 there, so it did fail closed, but
nothing in the line said so. Verified against a checksums file that does
not list the tarball: the build stops before the install.

Hugo exits 0 on an empty content tree and emits a two-page shell, which
would have been copied, pushed and deployed. The build asserts the home
page and a docs page exist.

`site/**` pull requests were never built. The only building job is gated
on `github.ref == 'refs/heads/master'`, so on a pull request every job
skipped and rendered in the checks list the same way a pass does, and the
image was first built on the run that also deploys it. A `validate` job
builds it with `push: false`, needing no secrets so it works on a fork.

`.github/dependabot.yml` watched `/site` for npm packages that are gone.
That entry is a docker one, which tracks the alpine base. It does not
track the hugo pin and cannot: the docker ecosystem reads `FROM`
references, and `ARG HUGO_VERSION` is a bare string in a download URL, so
that one is a manual bump and `site/README.md` says so.

`Dockerfile.dev` carries a `COPY`, so the dev image works without the
compose bind mount, and compose runs as the invoking user rather than
root, which on linux left root-owned `public/` and `resources/` in the
checkout.

Recorded in the backlog: `master` has `required_status_checks` off with an
empty check list, so the new job surfaces a red X and does not block a
merge. That is a settings decision rather than a code fix.
2026-08-21 18:05:56 -05:00

11 KiB

title
title
Authorization

OAuth Providers

Authentication is handled by external providers. You should set up OAuth2 for at least one to allow users to comment. It is not mandatory to have all of them, but one should be correctly configured.

Apple

  1. Log in to the developer account.
  2. If you don't have an App ID yet, create one. Later on, you'll need TeamID, which is an "App ID Prefix" value.
  3. Enable the "Sign in with Apple" capability for your App ID in the Certificates, Identifiers & Profiles section.
  4. Create Service ID and bind with App ID from the previous step. Apple will display the description field value to end-users on sign-in. You'll need that service Identifier as a ClientID later on.
  5. Configure "Sign in with Apple" for created Service ID. Add the domain where you will use that auth to "Domains and subdomains" and its Return URLs (like https://example.com/auth/apple/callback to "Return URLs".
  6. Register a New Key (private key) for the "Sign in with Apple" feature and download it, you'll need to put it to /srv/var/apple.p8 path inside the container. Also, write down the private Key ID.
  7. Add your Remark42 domain name and sender email in the Certificates, Identifiers & Profiles >> More section as a new Email Source.

After completing the previous steps, you can configure the Apple auth provider. You'll need to set the following environment variables:

  • AUTH_APPLE_CID (required) - Client ID (App ID or Services ID)
  • AUTH_APPLE_TID (required) - Team ID
  • AUTH_APPLE_KID (required) - Private Key ID
  • AUTH_APPLE_PRIVATE_KEY_FILEPATH (default /srv/var/apple.p8) - Private key file location

Facebook

  1. Open the list of apps on the Facebook Developers Platform
  2. Create a new app with this manual or use an existing app
  3. Open your app and choose "Facebook Login" and then "Web"
  4. Set "Site URL" to your domain, e.g., https://remark42.mysite.com
  5. Under "Facebook login"/"Settings" fill in "Valid OAuth redirect URIs" with your callback URL constructed as domain plus /auth/facebook/callback, e.g. https://remark42.mysite.com/auth/facebook/callback
  6. Select "App Review" and turn the public flag on. This step may ask you to provide a link to your privacy policy
  7. Write down the client ID and secret as AUTH_FACEBOOK_CID and AUTH_FACEBOOK_CSEC

GitHub

  1. Create a new "OAuth App": https://github.com/settings/developers
  2. Fill "Application Name" and "Homepage URL" for your site
  3. Under "Authorization callback URL" enter the correct URL constructed as domain + /auth/github/callback, i.e., https://remark42.mysite.com/auth/github/callback
  4. Take note of the Client ID (as AUTH_GITHUB_CID) and Client Secret (AUTH_GITHUB_CSEC)

Google

  1. Create a new project: https://console.cloud.google.com/projectcreate

  2. Choose the new project from the top right project dropdown (only if another project is selected)

  3. In the project Dashboard center pane, choose "APIs & Services"

  4. In the left Nav pane, choose "Credentials"

  5. In the center pane, choose the "OAuth consent screen" tab.

    • Select "External" and click "Create"
    • Fill in "App name" and select User support email
    • Upload a logo, if you want to
    • In the App Domain section:
      • Application home page - your site URL, e.g., https://mysite.com
      • Application privacy policy link - /web/privacy.html of your Remark42 installation, e.g. https://remark42.mysite.com/web/privacy.html (please check that it works)
      • Terms of service - leave empty
    • Authorized domains - your site domain, e.g., mysite.com
    • Developer contact information - add your email, and then click Save and continue
    • On the Scopes tab, just click Save and continue
    • On the Test users, add your email, then click Save and continue
    • Before going to the next step, set the app to "Production" and send it to verification
  6. In the center pane, choose the "Credentials" tab

    • Open the "Create credentials" drop-down
    • Choose "OAuth client ID"
    • Choose "Web application"
    • Application Name is freeform; choose something appropriate, like "Comments on mysite.com"
    • Authorized JavaScript Origins should be your domain, e.g., https://remark42.mysite.com
    • Authorized redirect URIs is the location of OAuth2/callback constructed as domain + /auth/google/callback, e.g., https://remark42.mysite.com/auth/google/callback
    • Click "Create"
  7. Take note of the Client ID (AUTH_GOOGLE_CID) and Client Secret (AUTH_GOOGLE_CSEC)

instructions for Google OAuth2 setup borrowed from oauth2_proxy

Microsoft

  1. Register a new application using the Azure portal
  2. Under "Supported account types" select "Accounts in any organizational directory (Any Microsoft Entra directory - Multitenant) and personal Microsoft accounts"
  3. Under "Authentication/Platform configurations/Web" enter the correct URL constructed as domain + /auth/microsoft/callback, i.e., https://example.mysite.com/auth/microsoft/callback
  4. In "Overview" take note of the Application (client) ID (AUTH_MICROSOFT_CID)
  5. Choose the new project from the top right project dropdown (only if another project is selected)
  6. Select "Certificates & secrets" and click on "+ New Client Secret" (AUTH_MICROSOFT_CSEC)

By default, Remark42 authenticates against the common endpoint, which requires a multi-tenant application. A single-tenant application (signInAudience: AzureADMyOrg) is rejected there with AADSTS50194. The account types the application actually accepts are the intersection of the endpoint and the registration, so selecting personal Microsoft accounts as well is what makes them usable; choose signInAudience: AzureADMultipleOrgs instead to keep sign-in to work or school accounts only. You can check and change the value under "Manifest" for an application that already exists.

To use a different endpoint, set AUTH_MICROSOFT_TENANT to your tenant ID or domain name for a single-tenant Entra ID application (AzureADMyOrg), to organizations for a multi-tenant application accepting work or school accounts only (AzureADMultipleOrgs), or to consumers for an application limited to personal Microsoft accounts (PersonalMicrosoftAccount).

Yandex

  1. Create a new "OAuth App": https://oauth.yandex.com/client/new
  2. Fill "App name" for your site
  3. Under Platforms select "Web services" and enter "Callback URI #1" constructed as domain + /auth/yandex/callback, i.e., https://remark42.mysite.com/auth/yandex/callback
  4. Select Permissions. You need the following permissions only from the "Yandex.Passport API" section:
  • Access to the user avatar
  • Access to username, first name and surname, gender
  1. Fill out the rest of the fields if needed
  2. Take note of the ID (AUTH_YANDEX_CID) and Password (AUTH_YANDEX_CSEC)

For more details refer to Yandex OAuth and Yandex.Passport API documentation.

Patreon

  1. Create a new Patreon client https://www.patreon.com/portal/registration/register-clients
  2. Fill App Name, Description
  3. In the field Redirect URIs enter the correct URI constructed as domain + /auth/patreon/callback, i.e., https://example.mysite.com/auth/patreon/callback
  4. Expand client details and note the Client ID and Client Secret. Those will be used as AUTH_PATREON_CID and AUTH_PATREON_CSEC

Discord Auth Provider

  1. Click on New Application to create Oauth client https://discord.com/developers/applications
  2. After filling "NAME", navigate to "OAuth2" option on the left sidebar
  3. Under "Redirects" enter the correct url constructed as domain + /auth/discord/callback. ie https://remark42.mysite.com/auth/discord/callback
  4. Take note of the CLIENT ID and CLIENT SECRET, as they are values for AUTH_DISCORD_CID and AUTH_DISCORD_CSEC respectively

Custom OAuth2 Provider

You can configure any OAuth2-compatible provider by setting these variables:

  • AUTH_CUSTOM_NAME - provider name used in auth routes
  • AUTH_CUSTOM_CID - OAuth client ID
  • AUTH_CUSTOM_CSEC - OAuth client secret
  • AUTH_CUSTOM_AUTH_URL - authorization endpoint
  • AUTH_CUSTOM_TOKEN_URL - token endpoint
  • AUTH_CUSTOM_INFO_URL - user info endpoint
  • AUTH_CUSTOM_SCOPES - optional scopes, comma-separated
  • AUTH_CUSTOM_ID_FIELD - optional user info field used as unique id (default sub)
  • AUTH_CUSTOM_NAME_FIELD - optional user info field used as display name (default name)
  • AUTH_CUSTOM_PICTURE_FIELD - optional user info field used as avatar URL (default picture)
  • AUTH_CUSTOM_EMAIL_FIELD - optional user info field used as email (default email)

Callback URL format:

https://<remark42-url>/auth/<AUTH_CUSTOM_NAME>/callback

Notes:

  • AUTH_CUSTOM_NAME must match ^[a-z0-9][a-z0-9_-]*$ and should not conflict with built-in providers: email, anonymous, google, github, facebook, yandex, twitter, microsoft, patreon, discord, telegram, dev, apple.
  • If any required custom variable is missing, Remark42 will fail to start.
  • Remark42 currently supports only one custom OAuth2 provider at a time.

Telegram

  1. Contact @BotFather and follow his instructions to create your bot (call it, for example, "My site auth bot")
  2. Write down the resulting token as TELEGRAM_TOKEN into remark42 config, and also set AUTH_TELEGRAM to true to enable telegram auth for your users.

Anonymous

Optionally, anonymous access can be turned on. In this case, an extra anonymous provider will allow logins without any social login with any name satisfying two conditions:

  • the name should be at least three characters long
  • the name has contains only letters, numbers, underscores and spaces