{"record":{"id":"03d3535d97c372bf","repo":"semaphoreui/semaphore","slug":"unauthorized","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"api/auth.go","lineNumber":335,"sourceCode":"\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc metricsAuthMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tusername := util.Config.Metrics.Username\n\t\tpassword := util.Config.Metrics.Password\n\n\t\treqUser, reqPass, ok := r.BasicAuth()\n\t\tuserMatch := subtle.ConstantTimeCompare([]byte(reqUser), []byte(username)) == 1\n\t\tpassMatch := subtle.ConstantTimeCompare([]byte(reqPass), []byte(password)) == 1\n\n\t\tif !util.Config.Metrics.Enabled || username == \"\" || password == \"\" || !ok || !userMatch || !passMatch {\n\t\t\tw.Header().Set(\"WWW-Authenticate\", `Basic realm=\"metrics\"`)\n\t\t\thttp.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\n// isStateChangingMethod reports whether an HTTP method can modify server state\n// and therefore requires CSRF protection. Safe methods (GET, HEAD, OPTIONS,\n// TRACE) are excluded.\nfunc isStateChangingMethod(method string) bool {\n\tswitch method {\n\tcase http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/api/auth.go#L317-L353","documentation":"This is the HTTP 401 response returned by the /metrics endpoint's Basic Auth middleware in Semaphore's API. The middleware wraps the Prometheus metrics handler and requires a username/password pair (from the metrics section of the config) to be supplied via HTTP Basic authentication, compared using constant-time comparison to prevent timing attacks. If metrics are disabled, credentials are not configured, or the supplied Basic Auth credentials do not match, it responds 'Unauthorized' with a WWW-Authenticate challenge header.","triggerScenarios":"GET /metrics without an Authorization: Basic header; with a malformed Basic header; with a username or password that does not match util.Config.Metrics (user/password); or when util.Config.Metrics.Enabled is false / username or password are empty strings in config.","commonSituations":"Monitoring systems (Prometheus, Grafana agent, curl health checks) scraping /metrics without the scrape config's basic_auth credentials; Metrics.Enabled missing/false in config while still probing the endpoint; credentials rotated in config but not in the scraper; special characters in the password that are not correctly base64-encoded in the client.","solutions":["Set basic_auth credentials in the monitoring client (e.g. Prometheus scrape_config basic_auth user/password) to match the metrics user/password in Semaphore's config","Ensure util.Config.Metrics.Enabled is true and both username and password are non-empty in Semaphore's config","Test with: curl -u <user>:<pass> http://<host>:<port>/api/metrics","If credentials changed, restart/reload Semaphore and update the scraper's stored credentials"],"exampleFix":"# before (Prometheus scrape without auth -> 401)\nscrape_configs:\n  - job_name: semaphore\n    static_configs:\n      - targets: ['semaphore:3000']\n# after\nscrape_configs:\n  - job_name: semaphore\n    basic_auth:\n      username: metrics_user\n      password: metrics_password\n    static_configs:\n      - targets: ['semaphore:3000']","handlingStrategy":"validation","validationCode":"// before scraping /metrics, verify credentials work:\nresp, err := http.Get(\"http://host:3000/api/metrics\") // expect 401 without auth\nreq, _ := http.NewRequest(\"GET\", \"http://host:3000/api/metrics\", nil)\nreq.SetBasicAuth(user, pass)\nresp, err = http.DefaultClient.Do(req)\nif err != nil || resp.StatusCode != 200 { /* fix credentials/config before relying on metrics */ }","typeGuard":"func metricsConfigValid(enabled bool, user, pass string) bool {\n    return enabled && user != \"\" && pass != \"\"\n}","tryCatchPattern":null,"preventionTips":["Store metrics credentials in the scraper config alongside the target URL","Keep config.Metrics.Enabled=true and credentials non-empty when monitoring is expected","Rotate scraper and server credentials together","Test the /metrics endpoint with curl -u after every config change"],"tags":["http","authentication","basic-auth","metrics"],"backgroundTag":"authentication-required","analyzedSha":"1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa","analyzedAt":"2026-09-07T11:00:33.293Z","contentChangedAt":"2026-09-07T11:00:33.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}