Go
Sign in with NIA — Go
Add “Sign in with NIA” to a Go service with coreos/go-oidc and golang.org/x/oauth2, using the standard OpenID Connect Authorization Code + PKCE flow.
01Overview
This guide adds “Sign in with NIA” to a Go application using the standard OpenID Connect flow. NIA is a conformant OIDC provider, so you use coreos/go-oidc together with golang.org/x/oauth2 — no NIA-specific SDK required.
Go runs as a confidential client: the browser is redirected to NIA to sign in, and your Go server exchanges the returned code for tokens using your client secret. go-oidc discovers NIA's endpoints and validates ID tokens against its JWKS; oauth2 handles the authorization-code exchange and PKCE.
- Flow: Authorization Code + PKCE (S256)
- Tokens: RS256 access & ID tokens, opaque refresh token
- You write: two handlers — login and callback — plus a session middleware
02Requirements
- Go 1.21+
- A NIA account with access to the client dashboard
- A registered OAuth client (
client_id+client_secret) - Your app's redirect URI registered on that client
- A way to persist session state (cookie store, e.g.
gorilla/sessions)
03Installation
Add the OIDC and OAuth2 modules to your go.mod. go-oidc fetches the discovery document and JWKS over HTTP; oauth2 drives the token exchange.
go get github.com/coreos/go-oidc/v3/oidc golang.org/x/oauth204Required OAuth library
Create the OIDC provider once at startup by pointing it at NIA's issuer. The provider fetches the discovery document and exposes NIA's oauth2.Endpoint; wrap it in an oauth2.Config and derive a token verifier from it.
package auth
import (
"context"
"os"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
var (
Provider *oidc.Provider
OAuth2Config oauth2.Config
Verifier *oidc.IDTokenVerifier
)
func Init(ctx context.Context) error {
provider, err := oidc.NewProvider(ctx, os.Getenv("NIA_ISSUER"))
if err != nil {
return err
}
OAuth2Config = oauth2.Config{
ClientID: os.Getenv("NIA_CLIENT_ID"),
ClientSecret: os.Getenv("NIA_CLIENT_SECRET"),
RedirectURL: "http://localhost:8080/auth/callback",
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "email", "profile"},
}
Provider = provider
Verifier = provider.Verifier(&oidc.Config{ClientID: OAuth2Config.ClientID})
return nil
}offline_access to Scopes only if you need refresh tokens (see step 13).05Environment Variables
Never hard-code secrets. Load them from the environment (e.g. with joho/godotenv in development) before calling auth.Init.
NIA_ISSUER=https://dev-api.niaauth.com
NIA_CLIENT_ID=your-client-id
NIA_CLIENT_SECRET=your-client-secret
SESSION_SECRET=replace-with-a-long-random-value06Register OAuth Client
In the NIA dashboard, create the client your Go app will authenticate as:
- Open Dashboard → Clients → New client
- Client type: Web (confidential)
- Allowed scopes:
openidemailprofile(addoffline_accessfor refresh) - Add your redirect URI (next step)
- Copy the
client_idandclient_secret— the secret is shown once
07Redirect URI
NIA matches redirect URIs exactly — scheme, host, port, and path must be identical, with no trailing-slash difference. Register one URI per environment.
# Development
http://localhost:8080/auth/callback
# Production
https://app.example.com/auth/callbackhttp://localhost:8080/auth/callback/ (trailing slash) will be rejected.08Issuer
The issuer is the single URL every OIDC library needs. Its discovery document advertises all endpoints below.
Issuer: https://dev-api.niaauth.com
Discovery: https://dev-api.niaauth.com/.well-known/openid-configuration
Authorization: https://dev-api.niaauth.com/oauth/authorize
Token: https://dev-api.niaauth.com/oauth/token
UserInfo: https://dev-api.niaauth.com/oauth/userinfo
JWKS: https://dev-api.niaauth.com/oauth/jwks.json09Authorization Flow
Add a login handler that generates state and a PKCE code verifier, stores both in a cookie, and redirects to NIA. x/oauth2's built-in PKCE helpers do the challenge computation for you.
package auth
import (
"crypto/rand"
"encoding/base64"
"net/http"
"golang.org/x/oauth2"
)
func randString() string {
b := make([]byte, 24)
rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
state := randString()
verifier := oauth2.GenerateVerifier()
// Store state + verifier so the callback can validate them.
http.SetCookie(w, &http.Cookie{Name: "nia_state", Value: state, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: 600})
http.SetCookie(w, &http.Cookie{Name: "nia_verifier", Value: verifier, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: 600})
url := OAuth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
http.Redirect(w, r, url, http.StatusFound)
}10Token Exchange
On callback, verify state, exchange the code with the stored PKCE verifier, and extract the raw ID token from the response before verifying it.
func CallbackHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
stateCookie, err := r.Cookie("nia_state")
if err != nil || r.URL.Query().Get("state") != stateCookie.Value {
http.Error(w, "invalid state", http.StatusBadRequest)
return
}
verifierCookie, err := r.Cookie("nia_verifier")
if err != nil {
http.Error(w, "missing pkce verifier", http.StatusBadRequest)
return
}
oauth2Token, err := OAuth2Config.Exchange(
ctx,
r.URL.Query().Get("code"),
oauth2.VerifierOption(verifierCookie.Value),
)
if err != nil {
http.Error(w, "token exchange failed", http.StatusInternalServerError)
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "no id_token in response", http.StatusInternalServerError)
return
}
idToken, err := Verifier.Verify(ctx, rawIDToken)
if err != nil {
http.Error(w, "id_token verification failed", http.StatusUnauthorized)
return
}
var claims struct {
Sub string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
}
if err := idToken.Claims(&claims); err != nil {
http.Error(w, "failed to parse claims", http.StatusInternalServerError)
return
}
SaveSession(w, r, claims.Sub, claims.Email, claims.Name, oauth2Token)
http.Redirect(w, r, "/", http.StatusFound)
}11JWKS Validation
NIA signs ID tokens with RS256. provider.Verifier(...).Verify validates the id_token against NIA's JWKS (fetched from discovery), checking the signature, iss, aud, and exp automatically — you never verify manually.
idToken, err := Verifier.Verify(ctx, rawIDToken)
if err != nil {
// signature (RS256, via JWKS), issuer, audience and expiry
// have already been checked — err tells you which one failed.
return err
}
// idToken.Issuer == os.Getenv("NIA_ISSUER")
// idToken.Audience contains OAuth2Config.ClientIDgo-oidc refetches JWKS by kid, rotation needs no code change on your side.12Session Management
Store the user and tokens in a server-side session (e.g. gorilla/sessions), then guard handlers with a small middleware.
package auth
import (
"net/http"
"github.com/gorilla/sessions"
"golang.org/x/oauth2"
)
var Store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
func SaveSession(w http.ResponseWriter, r *http.Request, sub, email, name string, token *oauth2.Token) {
sess, _ := Store.Get(r, "nia-session")
sess.Values["sub"] = sub
sess.Values["email"] = email
sess.Values["name"] = name
sess.Values["id_token"] = token.Extra("id_token")
sess.Values["refresh_token"] = token.RefreshToken
sess.Save(r, w)
}13Refresh Tokens
If you requested offline_access, NIA returns a refresh token. Use OAuth2Config.TokenSource to get an automatically-refreshing source, or exchange the refresh token manually. NIA rotates refresh tokens, so always persist the new one.
import "golang.org/x/oauth2"
// Automatic: TokenSource refreshes transparently when the access token expires.
func TokenSourceFor(ctx context.Context, refreshToken string) oauth2.TokenSource {
return OAuth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken})
}
// Manual: exchange the refresh token yourself.
func RefreshManually(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
src := OAuth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken})
newToken, err := src.Token()
if err != nil {
return nil, err
}
// NIA rotates the refresh token — persist newToken.RefreshToken.
return newToken, nil
}14Logout
Clear the session cookie and send the user to NIA's end-session endpoint so they are signed out of NIA too. Register your post_logout_redirect_uri on the client.
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
sess, _ := Store.Get(r, "nia-session")
idToken, _ := sess.Values["id_token"].(string)
sess.Options.MaxAge = -1
sess.Save(r, w)
q := url.Values{}
q.Set("id_token_hint", idToken)
q.Set("post_logout_redirect_uri", "http://localhost:8080/")
http.Redirect(w, r, os.Getenv("NIA_ISSUER")+"/oauth/logout?"+q.Encode(), http.StatusFound)
}15Deployment
Before going live:
- Register the production redirect URI (HTTPS) on the client
- Set all env vars via your host's secret manager — never commit
.env - Serve over HTTPS and set
Secure: trueon all cookies - Use a shared session store (e.g. Redis) behind multiple instances
- Update
OAuth2Config.RedirectURLto the production callback URL
OAuth2Config.RedirectURL = "https://app.example.com/auth/callback"
// Cookies must be Secure + SameSite=Lax behind HTTPS/a reverse proxy.
http.SetCookie(w, &http.Cookie{
Name: "nia_state",
Value: state,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})16Troubleshooting
- redirect_uri_mismatch — the URI must match the registered one exactly (no trailing slash, correct scheme/port).
- invalid_client — wrong
NIA_CLIENT_SECRET, or the client's token-auth method differs from whatoauth2.Configsends. - state/verifier cookie lost — the
nia_state/nia_verifiercookies didn't survive the redirect. CheckSameSite,Secure, and cookie domain settings. - invalid_grant / PKCE failed — the code verifier sent at token exchange doesn't match the challenge sent at login; usually a lost or overwritten cookie.
- id_token audience mismatch —
Verifierwas built with a differentClientIDthan the one used to start the flow. - ID-token signature / clock skew — sync server time via NTP;
go-oidcrejects tokens outside their validity window.