A confidential client — your Go service holds a real client_secret. Uses the standard library's net/http plus gorilla/sessions for server-side cookie
session state. Redirects to this platform's real hosted login page — see the integration guides overview — which shows every currently-real first-factor option; you never build a login form.
Two different hosts are used below: accounts.onehux.com for the hosted login/logout pages a browser is redirected to, and api-accounts.onehux.com for the actual API
calls your backend makes server-to-server — mixing these up is the single most common
integration mistake, since the wrong host doesn't error loudly, it just 404s.
Prefer the SDK — it already implements everything below (PKCE, the redirect, callback handling, token exchange, automatic refresh-token rotation, `/userinfo`, RP-initiated logout, OIDC Back-Channel Logout, and the public application launcher), real and maintained, rather than hand-rolling the raw API calls this page walks through manually:
go get github.com/Onehux/onehux-sso-go pkg.go.dev/github.com/Onehux/onehux-sso-go — the manual walkthrough below is still worth reading to understand what the SDK is doing on your behalf, and remains the reference if you're integrating in a language without an official SDK.
// auth.go
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"net/http"
"net/url"
"os"
"github.com/gorilla/sessions"
)
const onehuxLoginBaseURL = "https://your-org.onehux.com" // see "Two different hosts" above
const onehuxAPIBaseURL = "https://api.your-domain.com" // see "Two different hosts" above
const redirectURI = "https://yourapp.example.com/auth/callback"
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
func b64url(b []byte) string {
return base64.RawURLEncoding.EncodeToString(b)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
verifierBytes := make([]byte, 48)
rand.Read(verifierBytes)
codeVerifier := b64url(verifierBytes)
challenge := sha256.Sum256([]byte(codeVerifier))
codeChallenge := b64url(challenge[:])
stateBytes := make([]byte, 16)
rand.Read(stateBytes)
state := b64url(stateBytes)
session, _ := store.Get(r, "onehux-auth")
// PKCE verifier + state live server-side in the session — never in a cookie the
// browser can read, never round-tripped through the client.
session.Values["pkce_verifier"] = codeVerifier
session.Values["oauth_state"] = state
session.Save(r, w)
params := url.Values{
"client_id": {os.Getenv("ONEHUX_CLIENT_ID")},
"redirect_uri": {redirectURI},
"code_challenge": {codeChallenge},
"code_challenge_method": {"S256"},
"scope": {"openid profile email"},
"state": {state},
}
http.Redirect(w, r, onehuxLoginBaseURL+"/login?"+params.Encode(), http.StatusFound)
}import (
"bytes"
"encoding/json"
)
type tokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
}
func handleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
session, _ := store.Get(r, "onehux-auth")
expectedState, _ := session.Values["oauth_state"].(string)
codeVerifier, _ := session.Values["pkce_verifier"].(string)
delete(session.Values, "oauth_state")
delete(session.Values, "pkce_verifier")
if code == "" || state == "" || state != expectedState {
http.Error(w, "Invalid or missing OAuth state.", http.StatusBadRequest)
return
}
body, _ := json.Marshal(map[string]string{
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectURI,
"client_id": os.Getenv("ONEHUX_CLIENT_ID"),
"client_secret": os.Getenv("ONEHUX_CLIENT_SECRET"),
"code_verifier": codeVerifier,
})
resp, err := http.Post(onehuxAPIBaseURL+"/api/v1/oauth/token/", "application/json", bytes.NewReader(body))
if err != nil || resp.StatusCode != http.StatusOK {
http.Error(w, "Sign-in failed.", http.StatusBadRequest)
return
}
defer resp.Body.Close()
var tokens tokenResponse
json.NewDecoder(resp.Body).Decode(&tokens)
// Session establishment — the access token lives server-side only (the signed
// cookie session store), the same BFF discipline this platform's own dashboard
// follows on itself. The browser only ever gets the session cookie, never the raw
// token.
session.Values["access_token"] = tokens.AccessToken
session.Values["refresh_token"] = tokens.RefreshToken
session.Save(r, w)
http.Redirect(w, r, "/dashboard", http.StatusFound)
}A confidential client's refresh token rotates on every use and is itself long-lived
(30-day idle timeout, 30-day absolute cap — see Sessions),
so a 401 from /userinfo almost always means
just the 15-minute access token expired, not that the user needs to sign in again:
func refreshTokens(w http.ResponseWriter, r *http.Request, session *sessions.Session) string {
refreshToken, ok := session.Values["refresh_token"].(string)
if !ok || refreshToken == "" {
return ""
}
body, _ := json.Marshal(map[string]string{
"grant_type": "refresh_token",
"refresh_token": refreshToken,
"client_id": os.Getenv("ONEHUX_CLIENT_ID"),
"client_secret": os.Getenv("ONEHUX_CLIENT_SECRET"),
})
resp, err := http.Post(onehuxAPIBaseURL+"/api/v1/oauth/token/", "application/json", bytes.NewReader(body))
if err != nil || resp.StatusCode != http.StatusOK {
return ""
}
defer resp.Body.Close()
var tokens tokenResponse
json.NewDecoder(resp.Body).Decode(&tokens)
session.Values["access_token"] = tokens.AccessToken
session.Values["refresh_token"] = tokens.RefreshToken
session.Save(r, w)
return tokens.AccessToken
}
func handleMe(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "onehux-auth")
accessToken, ok := session.Values["access_token"].(string)
if !ok || accessToken == "" {
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
req, _ := http.NewRequest("GET", onehuxAPIBaseURL+"/api/v1/oauth/userinfo/", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode == http.StatusUnauthorized {
accessToken = refreshTokens(w, r, session)
if accessToken == "" {
delete(session.Values, "access_token")
delete(session.Values, "refresh_token")
session.Save(r, w)
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
req, _ = http.NewRequest("GET", onehuxAPIBaseURL+"/api/v1/oauth/userinfo/", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err = http.DefaultClient.Do(req)
}
if err != nil || resp.StatusCode != http.StatusOK {
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
io.Copy(w, resp.Body) // sub, name, email, picture, roles, permissions, ...
}Redirect to end the platform-wide session, not just clear your own local one — every other Application relying on that same session is signed out too:
func handleLogout(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "onehux-auth")
delete(session.Values, "access_token")
delete(session.Values, "refresh_token")
session.Save(r, w)
params := url.Values{
"client_id": {os.Getenv("ONEHUX_CLIENT_ID")},
"post_logout_redirect_uri": {"https://yourapp.example.com/logged-out"},
}
http.Redirect(w, r, onehuxLoginBaseURL+"/end-session?"+params.Encode(), http.StatusFound)
} Register this URL too: post_logout_redirect_uri is validated against
the exact same registered redirect_uris list as
your login callback — if https://yourapp.example.com/logged-out isn't in
that list too, /end-session rejects the request
with a real 400, even though your login callback
works fine.
The redirect above only notifies the app the user actually
clicked "log out" in. If they instead log out of a different app,
or directly at accounts.onehux.com, the
shared session is still revoked immediately and correctly — and if this Application
has registered a Back-Channel Logout endpoint, this platform pushes it a real,
spec-compliant logout_token (server-to-server,
HS256-signed with a dedicated backchannel signing secret — deliberately not your OAuth client_secret) as soon as the revocation
happens, rather than waiting for your app to notice on its own:
PATCH /api/v1/applications/{id}/backchannel-logout/
{ "backchannel_logout_uri": "https://yourapp.example.com/auth/backchannel-logout" }
-> { "backchannel_logout_uri": "...", "backchannel_logout_secret": "..." } (shown once) Receiving and verifying that push is its own integration surface — not covered by
this walkthrough. Without registering it, your app's own local session keeps
showing "signed in" until its next real /userinfo call fails, bounded by the access token's 15-minute lifetime. Don't treat a
locally-held session as a live signal of the IdP's true logout state unless you've
wired up the push.