Skills oidc-hosted-page-go
Implement "Sign in with SSO" in Go applications using SSOJet OIDC Authorization Code flow.
git clone https://github.com/ssojet/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/ssojet/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/authentication/oidc-hosted-page-go" ~/.claude/skills/ssojet-skills-oidc-hosted-page-go && rm -rf "$T"
skills/authentication/oidc-hosted-page-go/SKILL.mdImplement SSOJet OIDC (Go)
This expert AI assistant guide walks you through integrating "Sign in with SSO" functionality into an existing login page in a Go application using SSOJet as an OIDC identity provider. The goal is to modify the existing login flow to add SSO support without disrupting the current traditional login functionality (e.g., email/password).
1. Prerequisites
- An existing Go application (1.21+) with a login page.
- Basic knowledge of Go's
or a web framework likenet/http
orchi
.gorilla/mux - An active SSOJet account.
- SSO Connection Setup Guide
- Required packages:
,github.com/coreos/go-oidc/v3/oidc
.golang.org/x/oauth2
2. Implementation Steps
Step 1: Create Application in SSOJet
- Log in to the SSOJet Dashboard.
- Navigate to Applications.
- Create a new application (e.g., "MyGoApp", type Regular Web App).
- Configure the callback URI (e.g.,
).http://localhost:8080/auth/callback - Retrieve Client ID and Client Secret.
- Copy the Issuer URL from the Advanced > Endpoints section.
Step 2: Modify the Existing Go Project
Substep 2.1: Install Dependencies
Run the following commands to install the required packages:
go get github.com/coreos/go-oidc/v3/oidc go get golang.org/x/oauth2
Substep 2.2: Configure Environment Variables
Set the following environment variables (or use a
.env loader like godotenv):
SSOJET_ISSUER_URL=https://auth.ssojet.com SSOJET_CLIENT_ID=your_client_id SSOJET_CLIENT_SECRET=your_client_secret SSOJET_REDIRECT_URI=http://localhost:8080/auth/callback
Substep 2.3: Configure OIDC Provider
Create a dedicated file for OIDC configuration (e.g.,
internal/auth/oidc.go):
// internal/auth/oidc.go package auth import ( "context" "os" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" ) var ( OIDCProvider *oidc.Provider OAuth2Config oauth2.Config ) func InitOIDC() error { ctx := context.Background() provider, err := oidc.NewProvider(ctx, os.Getenv("SSOJET_ISSUER_URL")) if err != nil { return err } OIDCProvider = provider OAuth2Config = oauth2.Config{ ClientID: os.Getenv("SSOJET_CLIENT_ID"), ClientSecret: os.Getenv("SSOJET_CLIENT_SECRET"), RedirectURL: os.Getenv("SSOJET_REDIRECT_URI"), Endpoint: provider.Endpoint(), Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } return nil }
Substep 2.4: Update Login Page/UI
Create or modify your login page template (e.g.,
templates/login.html):
<!-- templates/login.html --> <!DOCTYPE html> <html> <head><title>Sign In</title></head> <body> <div class="login-container"> <h1>Sign In</h1> {{if .Error}} <p style="color: red;">{{.Error}}</p> {{end}} <form id="loginForm" method="POST" action="/auth/login"> <div> <label for="email">Email</label> <input type="email" id="email" name="email" required /> </div> <div id="passwordField"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <input type="hidden" id="isSSO" name="is_sso" value="false" /> <button type="submit" id="submitBtn">Sign In</button> </form> <button type="button" id="ssoToggle" onclick="toggleSSO()"> Sign in with SSO </button> </div> <script> function toggleSSO() { const isSSO = document.getElementById('isSSO'); const passwordField = document.getElementById('passwordField'); const submitBtn = document.getElementById('submitBtn'); const ssoToggle = document.getElementById('ssoToggle'); if (isSSO.value === 'false') { isSSO.value = 'true'; passwordField.style.display = 'none'; document.getElementById('password').removeAttribute('required'); submitBtn.textContent = 'Continue with SSO'; ssoToggle.textContent = 'Back to Password Login'; } else { isSSO.value = 'false'; passwordField.style.display = 'block'; document.getElementById('password').setAttribute('required', 'true'); submitBtn.textContent = 'Sign In'; ssoToggle.textContent = 'Sign in with SSO'; } } </script> </body> </html>
Substep 2.5: Update Backend Logic
Create the necessary handlers to process the OIDC flow.
1. Login Handler (
internal/auth/handlers.go):
// internal/auth/handlers.go package auth import ( "crypto/rand" "encoding/base64" "encoding/json" "log" "net/http" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" ) func generateState() string { b := make([]byte, 16) rand.Read(b) return base64.URLEncoding.EncodeToString(b) } // LoginHandler handles the login form submission. func LoginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } r.ParseForm() email := r.FormValue("email") isSSO := r.FormValue("is_sso") if isSSO == "true" { // Generate a random state for CSRF protection state := generateState() // Store state in a cookie http.SetCookie(w, &http.Cookie{ Name: "oidc_state", Value: state, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: 3600, }) // Build authorization URL with login_hint authURL := OAuth2Config.AuthCodeURL(state, oauth2.SetAuthURLParam("login_hint", email), ) http.Redirect(w, r, authURL, http.StatusFound) return } // Existing password login logic here log.Println("Processing traditional login...") http.Redirect(w, r, "/dashboard", http.StatusFound) }
2. Callback Handler (add to
internal/auth/handlers.go):
// CallbackHandler handles the OIDC callback. func CallbackHandler(w http.ResponseWriter, r *http.Request) { // Retrieve stored state from cookie stateCookie, err := r.Cookie("oidc_state") if err != nil { log.Println("State cookie not found:", err) http.Redirect(w, r, "/login?error=state_missing", http.StatusFound) return } // Verify state if r.URL.Query().Get("state") != stateCookie.Value { log.Println("State mismatch") http.Redirect(w, r, "/login?error=state_mismatch", http.StatusFound) return } // Exchange authorization code for token code := r.URL.Query().Get("code") token, err := OAuth2Config.Exchange(r.Context(), code) if err != nil { log.Println("Token exchange failed:", err) http.Redirect(w, r, "/login?error=token_exchange_failed", http.StatusFound) return } // Extract and verify ID token rawIDToken, ok := token.Extra("id_token").(string) if !ok { log.Println("No id_token in response") http.Redirect(w, r, "/login?error=no_id_token", http.StatusFound) return } verifier := OIDCProvider.Verifier(&oidc.Config{ClientID: OAuth2Config.ClientID}) idToken, err := verifier.Verify(r.Context(), rawIDToken) if err != nil { log.Println("ID token verification failed:", err) http.Redirect(w, r, "/login?error=token_verification_failed", http.StatusFound) return } // Extract user claims var claims map[string]interface{} if err := idToken.Claims(&claims); err != nil { log.Println("Failed to parse claims:", err) http.Redirect(w, r, "/login?error=claims_parse_failed", http.StatusFound) return } // Clear the state cookie http.SetCookie(w, &http.Cookie{ Name: "oidc_state", Value: "", Path: "/", MaxAge: -1, }) // TODO: Create a session for the user based on claims claimsJSON, _ := json.Marshal(claims) http.SetCookie(w, &http.Cookie{ Name: "user_session", Value: base64.URLEncoding.EncodeToString(claimsJSON), Path: "/", HttpOnly: true, MaxAge: 3600, }) log.Println("Authenticated User:", claims) // Redirect to the dashboard or intended page http.Redirect(w, r, "/dashboard", http.StatusFound) }
3. Main Application Setup (
main.go):
// main.go package main import ( "log" "net/http" "html/template" "yourmodule/internal/auth" ) func main() { // Initialize OIDC if err := auth.InitOIDC(); err != nil { log.Fatal("Failed to initialize OIDC:", err) } // Routes http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { tmpl := template.Must(template.ParseFiles("templates/login.html")) data := map[string]string{"Error": r.URL.Query().Get("error")} tmpl.Execute(w, data) }) http.HandleFunc("/auth/login", auth.LoginHandler) http.HandleFunc("/auth/callback", auth.CallbackHandler) http.HandleFunc("/dashboard", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("<h1>Dashboard</h1><p>Welcome!</p>")) }) log.Println("Server running on http://localhost:8080") log.Fatal(http.ListenAndServe(":8080", nil)) }
Step 3: Test the Modified Connection
- Start your application:
.go run main.go - Navigate to your login page (e.g.,
).http://localhost:8080/login - Verify that the traditional login form (Email + Password) is visible by default.
- Click "Sign in with SSO" and ensure:
- The password field disappears.
- The submit button changes to "Continue with SSO".
- Enter a test email and submit.
- You should be redirected to the SSOJet login page.
- Authenticate with SSOJet.
- You should be redirected back to
and then to/auth/callback
./dashboard
- You should be redirected back to
3. Additional Considerations
- Error Handling: Enhance the callback handler with granular OIDC error parsing.
- Styling: Adapt the example HTML/CSS to match your application's design system.
- Security: Use a proper session library (e.g.,
) instead of raw cookies in production.gorilla/sessions - Environment Variables: Use a library like
for local development and proper secrets management in production.godotenv
4. Support
- Contact SSOJet support: Reach out if you have integration questions.
- Check application logs: Use server-side logging to debug OIDC flow issues.
- Library Documentation: Refer to the go-oidc documentation and oauth2 documentation for advanced configuration.