semaphoreui/semaphore · error

You must be signed in to link an external account.

Error message

You must be signed in to link an external account.

What it means

In account-linking mode (?link=...), after passing the POST-method check the handler loads the user's session via getSession(r) and requires it to exist and be verified (session.IsVerified()). If there is no valid verified session, it returns HTTP 401 'You must be signed in to link an external account.' Linking an external identity requires an already-authenticated Semaphore account.

Solutions

  1. Sign in to Semaphore (completing any MFA/verification) before initiating account linking
  2. Ensure the linking POST request carries the session cookie (credentials: 'include' in cross-origin fetch)
  3. Re-authenticate if the session expired, then retry the linking flow
  4. Use the same browser/site context so SameSite cookie rules do not strip the session cookie

Example fix

// before: cookie not sent, 401
fetch('/api/auth/oidc/github?link=true', { method: 'POST' })
// after
fetch('/api/auth/oidc/github?link=true', { method: 'POST', credentials: 'include' })
Defensive patterns

Strategy: validation

Validate before calling

// check the user has a verified session before offering the link action:
const res = await fetch('/api/session/status', { credentials: 'include' })
if (!res.ok) { /* redirect to login first; linking requires a verified session */ }

Try / catch

const resp = await fetch(`/api/auth/oidc/${pid}?link=true`, { method: 'POST', credentials: 'include' })
if (resp.status === 401) {
    // send user through normal sign-in (incl. MFA), then retry linking
}

Prevention

When it happens

Trigger: POST /api/auth/oidc/{pid}?link=true without a session cookie; with an expired/invalidated session cookie; or with a session that is not verified (e.g. created but not yet passed verification/MFA step).

Common situations: User's session expired before clicking 'Link account'; session cookie blocked or stripped (same-site/cross-origin fetch without credentials); MFA not completed so the session is unverified; user attempting to link without ever signing in.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/73ade7c6bb38abdd. Report an issue: GitHub.

Appendix: source

Thrown at api/login.go:586

		log.Error(fmt.Errorf("no such provider: %s", pid))
		http.Error(w, "Unknown OIDC provider.", http.StatusNotFound)
		return
	}

	linkMode := r.URL.Query().Get("link") != ""

	if linkMode {
		// POST-only: SameSite=Lax attaches the session cookie to top-level
		// cross-site GET navigations, so a GET here would let an attacker
		// initiate linking (CSRF) and attach their IdP identity to the
		// victim's account. Lax never sends the cookie on cross-site POST.
		if r.Method != http.MethodPost {
			http.Error(w, "Account linking must be initiated with a POST request.", http.StatusMethodNotAllowed)
			return
		}
		session, ok := getSession(r)
		if !ok || !session.IsVerified() {
			http.Error(w, "You must be signed in to link an external account.", http.StatusUnauthorized)
			return
		}
	}

	returnValue := r.URL.Query().Get("return")
	if returnValue != "" {
		if config.ReturnViaState {
			returnPath = returnValue
		} else {
			redirectPath = returnValue
		}
	}

	_, oauth, err := getOidcProvider(pid, ctx, redirectPath)
	if err != nil {
		log.Error(err.Error())
		http.Error(w, "Failed to initialize OIDC provider. Contact your administrator.", http.StatusInternalServerError)
		return

View on GitHub (pinned to 1774ccb71a)