From 8ed89a6c6e97f5719a93c4e75995b7eba70fbe3a Mon Sep 17 00:00:00 2001 From: Martin <31348196+Earl0fPudding@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:50:33 +0200 Subject: [PATCH] feat(lib): Add option for adding difficulty field to JWT claims (#1063) * Add option for difficulty JWT field * Add DIFFICULTY_IN_JWT option to docs * Add missing_required_forwarded_headers to lt translation via Google Translate * docs(CHANGELOG): move CHANGELOG entry to the top Signed-off-by: Xe Iaso --------- Signed-off-by: Xe Iaso Co-authored-by: Xe Iaso --- cmd/anubis/main.go | 2 ++ docs/docs/CHANGELOG.md | 1 + docs/docs/admin/installation.mdx | 17 +++++++++-------- lib/anubis.go | 25 +++++++++++-------------- lib/config.go | 1 + lib/localization/locales/lt.json | 3 ++- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index e1bd36f..14f4560 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -51,6 +51,7 @@ var ( cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis") cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") + difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims") useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.") forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header") hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") @@ -433,6 +434,7 @@ func main() { CookieSecure: *cookieSecure, PublicUrl: *publicUrl, JWTRestrictionHeader: *jwtRestrictionHeader, + DifficultyInJWT: *difficultyInJWT, }) if err != nil { log.Fatalf("can't construct libanubis.Server: %v", err) diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 37a7874..219b6c3 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925)) - Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend. - Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines. +- Add the `DIFFICULTY_IN_JWT` option, which allows one to add the `difficulty` field in the JWT claims which indicates the difficulty of the token ([#1063](https://github.com/TecharoHQ/anubis/pull/1063)) ### Bug Fixes diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx index 58088e6..efb0fce 100644 --- a/docs/docs/admin/installation.mdx +++ b/docs/docs/admin/installation.mdx @@ -59,7 +59,7 @@ Currently the following settings are configurable via the policy file: Anubis uses these environment variables for configuration: | Environment Variable | Default value | Explanation | -|:-------------------------------|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. | | `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` | | `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. | @@ -70,6 +70,7 @@ Anubis uses these environment variables for configuration: | `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. | | `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false | | `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. | +| `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. | | `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. | | `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. | | `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. | @@ -100,14 +101,14 @@ If you don't know or understand what these settings mean, ignore them. These are ::: -| Environment Variable | Default value | Explanation | -| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FORCED_LANGUAGE` | unset | If set, forces Anubis to display challenge pages in the specified language instead of using the browser's Accept-Language header. Use ISO 639-1 language codes (e.g., `de` for German, `fr` for French). | +| Environment Variable | Default value | Explanation | +| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FORCED_LANGUAGE` | unset | If set, forces Anubis to display challenge pages in the specified language instead of using the browser's Accept-Language header. Use ISO 639-1 language codes (e.g., `de` for German, `fr` for French). | | `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. | -| `TARGET_DISABLE_KEEPALIVE` | `false` | If `true`, disables HTTP keep-alive for connections to the target backend. Useful for backends that don't handle keep-alive properly. | -| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | -| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. | -| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | +| `TARGET_DISABLE_KEEPALIVE` | `false` | If `true`, disables HTTP keep-alive for connections to the target backend. Useful for backends that don't handle keep-alive properly. | +| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. | +| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. | +| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. | diff --git a/lib/anubis.go b/lib/anubis.go index 7b8d4f1..6a44e1f 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -501,6 +501,12 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { var tokenString string // check if JWTRestrictionHeader is set and header is in request + claims := jwt.MapClaims{ + "challenge": chall.ID, + "method": rule.Challenge.Algorithm, + "policyRule": rule.Hash(), + "action": string(cr.Rule), + } if s.opts.JWTRestrictionHeader != "" { if r.Header.Get(s.opts.JWTRestrictionHeader) == "" { lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.") @@ -508,22 +514,13 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { s.respondWithError(w, r, "failed to sign JWT") return } else { - tokenString, err = s.signJWT(jwt.MapClaims{ - "challenge": chall.ID, - "method": rule.Challenge.Algorithm, - "policyRule": rule.Hash(), - "action": string(cr.Rule), - "restriction": internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)), - }) + claims["restriction"] = internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)) } - } else { - tokenString, err = s.signJWT(jwt.MapClaims{ - "challenge": chall.ID, - "method": rule.Challenge.Algorithm, - "policyRule": rule.Hash(), - "action": string(cr.Rule), - }) } + if s.opts.DifficultyInJWT { + claims["difficulty"] = rule.Challenge.Difficulty + } + tokenString, err = s.signJWT(claims) if err != nil { lg.Error("failed to sign JWT", "err", err) diff --git a/lib/config.go b/lib/config.go index 58c4a4a..c9437e6 100644 --- a/lib/config.go +++ b/lib/config.go @@ -46,6 +46,7 @@ type Options struct { Logger *slog.Logger PublicUrl string JWTRestrictionHeader string + DifficultyInJWT bool } func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) { diff --git a/lib/localization/locales/lt.json b/lib/localization/locales/lt.json index 798ea28..8887ad6 100644 --- a/lib/localization/locales/lt.json +++ b/lib/localization/locales/lt.json @@ -62,5 +62,6 @@ "js_iterations": "iteracijų", "js_finished_reading": "Viską perskaičiau, tęskime →", "js_calculation_error": "Skaičiavimo klaida!", - "js_calculation_error_msg": "Nepavyko įveikti iššūkio:" + "js_calculation_error_msg": "Nepavyko įveikti iššūkio:", + "missing_required_forwarded_headers": "Trūksta privalomų X-Forwarded-* antraščių" }