This is a sample Go web application demonstrating integration with MrWhoOidc as an OpenID Connect Provider. It showcases a confidential client implementation using Authorization Code flow with PKCE and native Go libraries.
This is the fastest way to see the demo in action.
git clone https://github.com/yourusername/MrWhoOidc.git
cd MrWhoOidc/MrWho/demos/go-client
# Start both the OIDC provider and demo client
docker compose -f ../docker-compose.yml -f docker-compose.demo.yml up -d
# Check logs
docker compose -f ../docker-compose.yml -f docker-compose.demo.yml logs -f go-demo
go-demoGo Web Client DemoConfidentialauthorization_code, refresh_tokenhttps://localhost:5080/callbackhttps://localhost:5080/openid, profile, emailEdit config.json and update the client secret:
{
"issuer": "https://localhost:8443",
"client_id": "go-demo",
"client_secret": "your-secret-from-admin-ui",
"redirect_uri": "https://localhost:5080/callback",
"post_logout_redirect_uri": "https://localhost:5080/",
"scopes": ["openid", "profile", "email"],
"server_port": 5080
}
Or use environment variables (see Configuration Reference below).
Restart the demo client:
docker compose -f ../docker-compose.yml -f docker-compose.demo.yml restart go-demo
admin@example.com / Admin123!)This approach lets you run the demo natively on your machine.
cd MrWhoOidc/MrWho
docker compose up -d
cd demos/go-client
go mod download
Copy config.example.json to config.json and update:
{
"issuer": "https://localhost:8443",
"client_id": "go-demo",
"client_secret": "your-secret-from-admin-ui",
"redirect_uri": "https://localhost:5080/callback",
"post_logout_redirect_uri": "https://localhost:5080/",
"scopes": ["openid", "profile", "email"],
"server_port": 5080
}
Follow step 3 from the Docker Compose guide above.
go run main.go
Or build and run:
go build -o go-web-client
./go-web-client
Navigate to https://localhost:5080
The demo supports JSON configuration (config.json) or environment variables:
| Environment Variable | config.json Key | Default | Description |
|---|---|---|---|
OIDC_ISSUER |
issuer |
https://localhost:8443 |
OIDC provider base URL |
OIDC_CLIENT_ID |
client_id |
go-demo |
Client identifier |
OIDC_CLIENT_SECRET |
client_secret |
(required) | Client secret from Admin UI |
OIDC_REDIRECT_URI |
redirect_uri |
https://localhost:5080/callback |
Callback URI |
OIDC_POST_LOGOUT_REDIRECT_URI |
post_logout_redirect_uri |
https://localhost:5080/ |
Post-logout URI |
OIDC_SCOPES |
scopes |
["openid","profile","email"] |
Requested scopes (JSON array) |
SERVER_PORT |
server_port |
5080 |
HTTP server port |
Priority: Environment variables override config.json values.
{
"issuer": "https://localhost:8443",
"client_id": "go-demo",
"client_secret": "your-client-secret",
"redirect_uri": "https://localhost:5080/callback",
"post_logout_redirect_uri": "https://localhost:5080/",
"scopes": ["openid", "profile", "email"],
"server_port": 5080
}
See CONFIG.md for detailed configuration guide.
You should see a page displaying:
/auth handler/authorize endpoint with:
response_type=codecode_challenge and code_challenge_method=S256redirect_uri, scope, state, nonce/callback with authorization code/logout endpoint with id_token_hint and post_logout_redirect_uriCause: Client not registered or client secret mismatch.
Solution:
client_id matches registrationclient_secret is correctCause: Redirect URI not registered in Admin UI.
Solution:
https://localhost:5080/callback is listed in client’s Redirect URIsCause: MrWhoOidc uses self-signed certificate.
Solution:
# Linux
sudo cp certs/aspnetapp.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certs/aspnetapp.crt
Cause: MrWhoOidc container not running.
Solution:
# Check MrWhoOidc is running
docker ps | grep mrwho-oidc
# Start if not running
cd MrWhoOidc/MrWho
docker compose up -d
Cause: ID token signature validation failed.
Solution:
Cause: Session storage issues or cookie problems.
Solution:
localhostfunc loadConfig() (*Config, error) {
// Read config file
data, err := os.ReadFile("config.json")
if err != nil {
return nil, err
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
// Override with environment variables
if issuer := os.Getenv("OIDC_ISSUER"); issuer != "" {
config.Issuer = issuer
}
// ... (other env vars)
return &config, nil
}
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, config.Issuer)
if err != nil {
log.Fatal(err)
}
oauth2Config := oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
RedirectURL: config.RedirectURI,
Endpoint: provider.Endpoint(),
Scopes: config.Scopes,
}
verifier := provider.Verifier(&oidc.Config{ClientID: config.ClientID})
func handleLogin(w http.ResponseWriter, r *http.Request) {
// Generate PKCE challenge
codeVerifier := generateCodeVerifier()
codeChallenge := generateCodeChallenge(codeVerifier)
// Generate state and nonce
state := generateRandomString(32)
nonce := generateRandomString(32)
// Store in session
session := getSession(r)
session.CodeVerifier = codeVerifier
session.State = state
session.Nonce = nonce
saveSession(w, session)
// Build authorization URL
authURL := oauth2Config.AuthCodeURL(state,
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
oauth2.SetAuthURLParam("nonce", nonce),
)
http.Redirect(w, r, authURL, http.StatusFound)
}
func handleCallback(w http.ResponseWriter, r *http.Request) {
// Validate state
session := getSession(r)
if r.URL.Query().Get("state") != session.State {
http.Error(w, "Invalid state", http.StatusBadRequest)
return
}
// Exchange code for tokens
ctx := context.Background()
code := r.URL.Query().Get("code")
token, err := oauth2Config.Exchange(ctx, code,
oauth2.SetAuthURLParam("code_verifier", session.CodeVerifier),
)
if err != nil {
http.Error(w, "Token exchange failed", http.StatusInternalServerError)
return
}
// Verify ID token
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token", http.StatusInternalServerError)
return
}
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
http.Error(w, "Token verification failed", http.StatusUnauthorized)
return
}
// Extract claims
var claims struct {
Sub string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
Nonce string `json:"nonce"`
}
if err := idToken.Claims(&claims); err != nil {
http.Error(w, "Failed to parse claims", http.StatusInternalServerError)
return
}
// Validate nonce
if claims.Nonce != session.Nonce {
http.Error(w, "Invalid nonce", http.StatusBadRequest)
return
}
// Save user info to session
session.IDToken = rawIDToken
session.AccessToken = token.AccessToken
session.RefreshToken = token.RefreshToken
session.UserInfo = claims
saveSession(w, session)
http.Redirect(w, r, "/", http.StatusFound)
}