MojoAuth Hosted Login Page with Go (Fiber)
Introduction
Go (Golang) is a statically typed, compiled programming language designed for simplicity, efficiency, and reliability. Fiber is a Go web framework inspired by Express.js that combines the familiarity of a Node.js-like API with the performance of Go. Fiber is built on top of Fasthttp, the fastest HTTP engine for Go, and provides a range of features for building high-performance web applications.
This guide demonstrates how to integrate MojoAuth's Hosted Login Page with a Go application using the Fiber framework and the standard OpenID Connect (OIDC) flow for secure authentication.
Links:
- MojoAuth Hosted Login Page Documentation (opens in a new tab)
- Go Programming Language (opens in a new tab)
- Fiber Framework (opens in a new tab)
Prerequisites
Before you begin, make sure you have:
- A MojoAuth account with an OIDC application configured
- Your OIDC Client ID, Client Secret, and Issuer URL (https://your-project.auth.mojoauth.com (opens in a new tab))
- Go 1.16+ installed
- Basic knowledge of Go and the Fiber framework
Project Setup
Let's start by creating a new Go project:
mkdir mojoauth-fiber-demo cd mojoauth-fiber-demo go mod init github.com/yourusername/mojoauth-fiber-demo
Install Required Dependencies
For OIDC authentication in Go with Fiber, we'll use the following packages:
go get github.com/gofiber/fiber/v2 go get github.com/gofiber/template/html go get github.com/gofiber/session/v2 go get github.com/coreos/go-oidc/v3/oidc go get golang.org/x/oauth2 go get github.com/joho/godotenv
Project Structure
Create the following project structure:
mojoauth-fiber-demo/ ├── config/ │ └── config.go ├── handlers/ │ └── auth.go ├── middleware/ │ └── auth.go ├── views/ │ ├── index.html │ ├── callback.html │ └── profile.html ├── main.go └── .env
Configure Environment Variables
Create a .env
file in the root of your project:
MOJOAUTH_CLIENT_ID=your-client-id MOJOAUTH_CLIENT_SECRET=your-client-secret MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com MOJOAUTH_REDIRECT_URI=http://localhost:3000/callback MOJOAUTH_LOGOUT_REDIRECT_URI=http://localhost:3000/ SESSION_SECRET=your-secure-random-string PORT=3000
Set Up Configuration
Create the configuration file in config/config.go
:
package config import ( "log" "os" "github.com/joho/godotenv" ) // Config holds all configuration for the application type Config struct { ClientID string ClientSecret string Issuer string RedirectURI string LogoutRedirectURI string SessionSecret string Port string } // LoadConfig loads configuration from environment variables func LoadConfig() *Config { // Load .env file if it exists err := godotenv.Load() if err != nil { log.Println("No .env file found, using environment variables") } // Set default port if not specified port := os.Getenv("PORT") if port == "" { port = "3000" } return &Config{ ClientID: os.Getenv("MOJOAUTH_CLIENT_ID"), ClientSecret: os.Getenv("MOJOAUTH_CLIENT_SECRET"), Issuer: os.Getenv("MOJOAUTH_ISSUER"), RedirectURI: os.Getenv("MOJOAUTH_REDIRECT_URI"), LogoutRedirectURI: os.Getenv("MOJOAUTH_LOGOUT_REDIRECT_URI"), SessionSecret: os.Getenv("SESSION_SECRET"), Port: port, } }
Create Authentication Handlers
Create the authentication handlers in handlers/auth.go
:
package handlers import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "github.com/coreos/go-oidc/v3/oidc" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/session" "golang.org/x/oauth2" "github.com/yourusername/mojoauth-fiber-demo/config" ) // AuthHandler handles all authentication-related requests type AuthHandler struct { oauth2Config *oauth2.Config verifier *oidc.IDTokenVerifier config *config.Config store *session.Store } // NewAuthHandler creates a new AuthHandler func NewAuthHandler(cfg *config.Config, store *session.Store) (*AuthHandler, error) { ctx := context.Background() provider, err := oidc.NewProvider(ctx, cfg.Issuer) if err != nil { return nil, fmt.Errorf("failed to get provider: %v", err) } oauth2Config := &oauth2.Config{ ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, RedirectURL: cfg.RedirectURI, Endpoint: provider.Endpoint(), Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } verifier := provider.Verifier(&oidc.Config{ClientID: cfg.ClientID}) return &AuthHandler{ oauth2Config: oauth2Config, verifier: verifier, config: cfg, store: store, }, nil } // generateRandomState generates a random string for OAuth2 state parameter func generateRandomState() (string, error) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { return "", err } state := base64.StdEncoding.EncodeToString(b) return state, nil } // Home renders the home page func (h *AuthHandler) Home(c *fiber.Ctx) error { sess, err := h.store.Get(c) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to get session") } var claims map[string]interface{} authenticated := false if userClaims := sess.Get("user_claims"); userClaims != nil { claims = userClaims.(map[string]interface{}) authenticated = true } return c.Render("index", fiber.Map{ "authenticated": authenticated, "claims": claims, }) } // Login initiates the OIDC authentication flow func (h *AuthHandler) Login(c *fiber.Ctx) error { state, err := generateRandomState() if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to generate state") } sess, err := h.store.Get(c) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to get session") } sess.Set("state", state) if err := sess.Save(); err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to save session") } authURL := h.oauth2Config.AuthCodeURL(state) return c.Redirect(authURL) } // Callback handles the OIDC callback func (h *AuthHandler) Callback(c *fiber.Ctx) error { sess, err := h.store.Get(c) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to get session") } // Verify state to prevent CSRF expectedState := sess.Get("state") if expectedState == nil { return c.Render("callback", fiber.Map{ "error": "State not found in session", }) } state := c.Query("state") if state != expectedState { return c.Render("callback", fiber.Map{ "error": "State mismatch", }) } // Exchange code for token code := c.Query("code") oauth2Token, err := h.oauth2Config.Exchange(context.Background(), code) if err != nil { return c.Render("callback", fiber.Map{ "error": "Failed to exchange code for token: " + err.Error(), }) } // Extract ID token rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { return c.Render("callback", fiber.Map{ "error": "No ID token found in OAuth2 token", }) } // Verify ID token idToken, err := h.verifier.Verify(context.Background(), rawIDToken) if err != nil { return c.Render("callback", fiber.Map{ "error": "Failed to verify ID token: " + err.Error(), }) } // Extract user claims var claims map[string]interface{} if err := idToken.Claims(&claims); err != nil { return c.Render("callback", fiber.Map{ "error": "Failed to extract claims: " + err.Error(), }) } // Store tokens and claims in session sess.Set("access_token", oauth2Token.AccessToken) sess.Set("id_token", rawIDToken) sess.Set("refresh_token", oauth2Token.RefreshToken) sess.Set("user_claims", claims) if err := sess.Save(); err != nil { return c.Render("callback", fiber.Map{ "error": "Failed to save session: " + err.Error(), }) } // Redirect to profile page return c.Redirect("/profile") } // Profile displays the user's profile information func (h *AuthHandler) Profile(c *fiber.Ctx) error { sess, err := h.store.Get(c) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to get session") } userClaims := sess.Get("user_claims") if userClaims == nil { return c.Redirect("/") } claims := userClaims.(map[string]interface{}) accessToken := sess.Get("access_token") idToken := sess.Get("id_token") // Format the claims for display claimsJSON, _ := json.MarshalIndent(claims, "", " ") return c.Render("profile", fiber.Map{ "claims": claims, "claimsJSON": string(claimsJSON), "accessToken": accessToken, "idToken": idToken, }) } // Logout logs the user out func (h *AuthHandler) Logout(c *fiber.Ctx) error { sess, err := h.store.Get(c) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to get session") } // Clear session if err := sess.Destroy(); err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to destroy session") } // Redirect to MojoAuth logout endpoint with client_id and post_logout_redirect_uri logoutURL := fmt.Sprintf( "%s/oauth2/sessions/logout?client_id=%s&post_logout_redirect_uri=%s", h.config.Issuer, h.config.ClientID, h.config.LogoutRedirectURI, ) return c.Redirect(logoutURL) } // ProtectedAPI handles protected API requests func (h *AuthHandler) ProtectedAPI(c *fiber.Ctx) error { sess, err := h.store.Get(c) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Failed to get session", }) } userClaims := sess.Get("user_claims") if userClaims == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Unauthorized", }) } claims := userClaims.(map[string]interface{}) return c.JSON(fiber.Map{ "message": "This is a protected API endpoint", "user": claims, }) }
Create Authentication Middleware
Create the authentication middleware in middleware/auth.go
:
package middleware import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/session" ) // Store is the session store used by the middleware var Store *session.Store // RequireAuth ensures the user is authenticated func RequireAuth() fiber.Handler { return func(c *fiber.Ctx) error { sess, err := Store.Get(c) if err != nil { return c.Redirect("/") } userClaims := sess.Get("user_claims") if userClaims == nil { return c.Redirect("/") } return c.Next() } } // APIAuth handles API authentication func APIAuth() fiber.Handler { return func(c *fiber.Ctx) error { sess, err := Store.Get(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Unauthorized", }) } userClaims := sess.Get("user_claims") if userClaims == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Unauthorized", }) } return c.Next() } }
Create HTML Templates
views/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Home - MojoAuth Fiber Demo</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f9fafb; } .container { background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } h1 { color: #00ACD7; margin-top: 0; } .btn { display: inline-block; background-color: #00ACD7; color: white; padding: 10px 20px; border-radius: 4px; text-decoration: none; font-weight: 500; transition: background-color 0.2s; } .btn:hover { background-color: #0098c0; } .features { display: flex; gap: 20px; margin: 30px 0; } .feature { flex: 1; background: #f3f4f6; padding: 15px; border-radius: 6px; } .feature h3 { margin-top: 0; } .welcome { background-color: #e0f7fa; padding: 15px; border-radius: 6px; margin-bottom: 20px; } </style> </head> <body> <div class="container"> <h1>MojoAuth Go/Fiber Demo</h1> {{if .authenticated}} <div class="welcome"> <h2>Welcome back!</h2> <p>You are logged in as {{index .claims "name"}}.</p> <a href="/profile" class="btn">View Profile</a> <a href="/logout" style="margin-left: 10px;">Logout</a> </div> {{else}} <p> This application demonstrates how to integrate MojoAuth's Hosted Login Page with a Go application using the Fiber framework and OpenID Connect. </p> <a href="/login" class="btn">Login with MojoAuth</a> <div class="features"> <div class="feature"> <h3>🔒 Secure Authentication</h3> <p>OpenID Connect provides a secure authentication layer on top of OAuth 2.0.</p> </div> <div class="feature"> <h3>🚀 High Performance</h3> <p>Fiber is built on top of Fasthttp, the fastest HTTP engine for Go.</p> </div> <div class="feature"> <h3>🔑 Passwordless Options</h3> <p>Support for email magic links, SMS OTP, and social login.</p> </div> </div> {{end}} </div> </body> </html>
views/callback.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Processing Login - MojoAuth Fiber Demo</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f9fafb; } .container { background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; } h1 { color: #00ACD7; margin-top: 0; } .error { background-color: #fee2e2; color: #b91c1c; padding: 15px; border-radius: 6px; margin: 20px 0; } .btn { display: inline-block; background-color: #00ACD7; color: white; padding: 10px 20px; border-radius: 4px; text-decoration: none; font-weight: 500; transition: background-color 0.2s; } .btn:hover { background-color: #0098c0; } .spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top: 4px solid #00ACD7; width: 40px; height: 40px; margin: 20px auto; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body> <div class="container"> {{if .error}} <h1>Authentication Error</h1> <div class="error"> <p>{{.error}}</p> </div> <a href="/" class="btn">Return to Home</a> {{else}} <h1>Processing Login</h1> <div class="spinner"></div> <p>Please wait while we complete your authentication...</p> {{end}} </div> </body> </html>
views/profile.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>User Profile - MojoAuth Fiber Demo</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f9fafb; } .container { background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } h1 { color: #00ACD7; margin-top: 0; } .profile-header { display: flex; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #e5e7eb; } .profile-avatar { width: 100px; height: 100px; border-radius: 50%; background-color: #00ACD7; color: white; display: flex; align-items: center; justify-content: center; font-size: 40px; font-weight: bold; margin-right: 20px; } .profile-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; } .profile-info h2 { margin-top: 0; margin-bottom: 5px; } .profile-email { color: #6b7280; margin: 0; } .btn { display: inline-block; background-color: #00ACD7; color: white; padding: 10px 20px; border-radius: 4px; text-decoration: none; font-weight: 500; transition: background-color 0.2s; } .btn:hover { background-color: #0098c0; } .btn-light { background-color: #e5e7eb; color: #4b5563; } .btn-light:hover { background-color: #d1d5db; } .section { margin-bottom: 30px; } .section h3 { margin-top: 0; color: #4b5563; font-size: 18px; } table { width: 100%; border-collapse: collapse; } table td { padding: 10px 0; border-bottom: 1px solid #e5e7eb; } table td:first-child { width: 30%; color: #6b7280; font-weight: 500; } .token-card { background-color: #f3f4f6; padding: 15px; border-radius: 6px; } .token-display { background-color: #e5e7eb; padding: 10px; border-radius: 4px; font-family: monospace; overflow-x: auto; white-space: pre-wrap; margin-bottom: 15px; } pre { background-color: #f3f4f6; padding: 15px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; font-size: 14px; } .actions { display: flex; gap: 10px; margin-top: 30px; } </style> </head> <body> <div class="container"> <h1>User Profile</h1> <div class="profile-header"> {{if index .claims "picture"}} <div class="profile-avatar"> <img src="{{index .claims "picture"}}" alt="Profile picture"> </div> {{else}} <div class="profile-avatar"> {{slice (index .claims "name") 0 1}} </div> {{end}} <div class="profile-info"> <h2>{{index .claims "name"}}</h2> <p class="profile-email">{{index .claims "email"}}</p> </div> </div> <div class="section"> <h3>Profile Information</h3> <table> <tr> <td>User ID:</td> <td>{{index .claims "sub"}}</td> </tr> {{if index .claims "name"}} <tr> <td>Name:</td> <td>{{index .claims "name"}}</td> </tr> {{end}} {{if index .claims "email"}} <tr> <td>Email:</td> <td>{{index .claims "email"}}</td> </tr> {{end}} {{if index .claims "email_verified"}} <tr> <td>Email Verified:</td> <td>{{if index .claims "email_verified"}}Yes{{else}}No{{end}}</td> </tr> {{end}} </table> </div> <div class="section"> <h3>Token Information</h3> <div class="token-card"> <h4>ID Token</h4> <div class="token-display">{{slice .idToken 0 40}}...</div> <h4>Access Token</h4> <div class="token-display">{{slice .accessToken 0 40}}...</div> </div> </div> <div class="section"> <h3>Raw Profile Data</h3> <pre>{{.claimsJSON}}</pre> </div> <div class="actions"> <a href="/" class="btn">Home</a> <a href="/logout" class="btn btn-light">Logout</a> </div> </div> </body> </html>
Create Main Application
Now, create the main application file main.go
:
package main import ( "fmt" "log" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/session" "github.com/gofiber/template/html" "github.com/yourusername/mojoauth-fiber-demo/config" "github.com/yourusername/mojoauth-fiber-demo/handlers" "github.com/yourusername/mojoauth-fiber-demo/middleware" ) func main() { // Load configuration cfg := config.LoadConfig() // Initialize template engine engine := html.New("./views", ".html") // Create new Fiber app app := fiber.New(fiber.Config{ Views: engine, }) // Add middleware app.Use(logger.New()) // Initialize session store store := session.New(session.Config{ CookieHTTPOnly: true, CookieSecure: false, // Set to true in production with HTTPS Expiration: 86400 * 1000, // 24 hours KeyLookup: "cookie:mojoauth_session", CookiePath: "/", }) // Make session store available to middleware middleware.Store = store // Create auth handler authHandler, err := handlers.NewAuthHandler(cfg, store) if err != nil { log.Fatalf("Failed to create auth handler: %v", err) } // Static files app.Static("/static", "./static") // Routes app.Get("/", authHandler.Home) app.Get("/login", authHandler.Login) app.Get("/callback", authHandler.Callback) app.Get("/logout", authHandler.Logout) // Protected routes app.Get("/profile", middleware.RequireAuth(), authHandler.Profile) // API routes api := app.Group("/api", middleware.APIAuth()) api.Get("/protected", authHandler.ProtectedAPI) // Start server addr := fmt.Sprintf(":%s", cfg.Port) log.Printf("Starting server on http://localhost%s", addr) if err := app.Listen(addr); err != nil { log.Fatalf("Server failed to start: %v", err) } }
Create the Static Directory
mkdir -p static/css
You can add any static files like CSS, JavaScript, and images here if needed.
Testing the Authentication Flow
- Start your Go application:
go run main.go
-
Open your browser and navigate to
http://localhost:3000
-
Click the "Login with MojoAuth" button
-
You'll be redirected to the MojoAuth Hosted Login Page
-
After successful authentication, you'll be redirected back to your application
-
You should now see your user profile information
Making Authenticated API Requests
You can now make authenticated API requests to your /api/protected
endpoint:
# First login through the web interface, then use the access token curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" http://localhost:3000/api/protected
Production Considerations
1. Environment Configuration
For production deployment, use environment variables or a secure secret management system instead of a .env
file:
export MOJOAUTH_CLIENT_ID=your-client-id export MOJOAUTH_CLIENT_SECRET=your-client-secret export MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com export MOJOAUTH_REDIRECT_URI=https://your-domain.com/callback export MOJOAUTH_LOGOUT_REDIRECT_URI=https://your-domain.com/ export SESSION_SECRET=your-secure-random-string export PORT=3000
2. HTTPS in Production
Always use HTTPS in production. You can configure your Go application to use TLS directly:
// In main.go import ( "crypto/tls" ) // Configure TLS certFile := "./cert/cert.pem" keyFile := "./cert/key.pem" // Create TLS configuration tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, } // Start server with TLS log.Printf("Starting server with TLS on https://localhost%s", addr) if err := app.ListenTLS(addr, certFile, keyFile, tlsConfig); err != nil { log.Fatalf("Server failed to start: %v", err) }
Alternatively, use a reverse proxy like Nginx or Caddy to handle TLS termination.
3. Session Security
For production, configure more secure session settings:
store := session.New(session.Config{ KeyLookup: "cookie:mojoauth_session", CookieSecure: true, // Require HTTPS CookieHTTPOnly: true, CookieSameSite: "Lax", CookieDomain: "your-domain.com", // Set to your domain CookiePath: "/", Expiration: 86400 * 1000, // 24 hours })
For large applications, consider using Redis for session storage:
go get github.com/gofiber/storage/redis
import ( "github.com/gofiber/storage/redis" ) // Initialize Redis storage redisStorage := redis.New(redis.Config{ Host: "127.0.0.1", Port: 6379, Username: "", Password: "", Database: 0, }) // Use Redis storage for sessions store := session.New(session.Config{ Storage: redisStorage, // Other session options })
4. Structured Logging
Implement structured logging for better production monitoring:
go get github.com/sirupsen/logrus
import ( "github.com/sirupsen/logrus" ) // Initialize logger logger := logrus.New() logger.SetFormatter(&logrus.JSONFormatter{}) if os.Getenv("ENVIRONMENT") == "production" { logger.SetLevel(logrus.InfoLevel) } else { logger.SetLevel(logrus.DebugLevel) } // Use logger in your application logger.WithFields(logrus.Fields{ "user_id": userID, "action": "login", }).Info("User logged in")
5. Token Validation
In production, implement more robust token validation:
// In middleware/auth.go import ( "context" "strings" "github.com/coreos/go-oidc/v3/oidc" "github.com/gofiber/fiber/v2" ) // BearerAuth middleware for API endpoints using Bearer token func BearerAuth(verifier *oidc.IDTokenVerifier) fiber.Handler { return func(c *fiber.Ctx) error { // Get token from Authorization header authHeader := c.Get("Authorization") if authHeader == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "No Authorization header provided", }) } // Extract the token parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Invalid Authorization header format", }) } token := parts[1] // Verify the token _, err := verifier.Verify(context.Background(), token) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Invalid token", }) } // Continue to the next handler return c.Next() } }
Advanced Features
1. Token Refresh
Implement token refresh to maintain user sessions:
// In handlers/auth.go // RefreshToken refreshes the access token func (h *AuthHandler) RefreshToken(c *fiber.Ctx) error { sess, err := h.store.Get(c) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Failed to get session", }) } refreshToken := sess.Get("refresh_token") if refreshToken == nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "No refresh token available", }) } token := &oauth2.Token{ RefreshToken: refreshToken.(string), } newToken, err := h.oauth2Config.TokenSource(context.Background(), token).Token() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Failed to refresh token: %v", err), }) } // Extract ID token rawIDToken, ok := newToken.Extra("id_token").(string) if !ok { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "No ID token found in refreshed token", }) } // Update session with new tokens sess.Set("access_token", newToken.AccessToken) sess.Set("id_token", rawIDToken) sess.Set("refresh_token", newToken.RefreshToken) if err := sess.Save(); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": fmt.Sprintf("Failed to save session: %v", err), }) } // Return success response return c.JSON(fiber.Map{ "status": "success", "message": "Token refreshed successfully", }) }
Add the route to your main application:
app.Post("/refresh-token", middleware.RequireAuth(), authHandler.RefreshToken)
2. Role-Based Access Control
Implement role-based access control using claims from the ID token:
// In middleware/auth.go // HasRole checks if a user has a specific role func HasRole(claims map[string]interface{}, role string) bool { // Check if the claims contain roles rolesInterface, ok := claims["roles"] if !ok { return false } // Check if roles is a slice roles, ok := rolesInterface.([]interface{}) if !ok { return false } // Check if the role exists in the slice for _, r := range roles { if roleStr, ok := r.(string); ok && roleStr == role { return true } } return false } // RequireRole middleware ensures the user has a specific role func RequireRole(role string) fiber.Handler { return func(c *fiber.Ctx) error { sess, err := Store.Get(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Unauthorized", }) } userClaims := sess.Get("user_claims") if userClaims == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Unauthorized", }) } claims := userClaims.(map[string]interface{}) if !HasRole(claims, role) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "Forbidden: Insufficient permissions", }) } return c.Next() } }
Use the middleware for role-protected routes:
// Admin routes admin := app.Group("/admin", middleware.RequireAuth(), middleware.RequireRole("admin")) admin.Get("/dashboard", adminHandler.Dashboard)
3. User Management Service
Create a separate service for user management operations:
// userservice/userservice.go package userservice import ( "context" "encoding/json" "errors" "net/http" ) // UserService provides user management functionality type UserService struct { issuerURL string } // NewUserService creates a new UserService func NewUserService(issuerURL string) *UserService { return &UserService{ issuerURL: issuerURL, } } // GetUserInfo retrieves user information from the UserInfo endpoint func (s *UserService) GetUserInfo(accessToken string) (map[string]interface{}, error) { if accessToken == "" { return nil, errors.New("no access token provided") } // Create request to UserInfo endpoint req, err := http.NewRequest("GET", s.issuerURL+"/oauth/userinfo", nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New("failed to get user info") } var userInfo map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { return nil, err } return userInfo, nil }
Next Steps
- Implement role-based access control using claims from the ID token
- Add support for multiple authentication methods (passwordless, social login)
- Enhance error handling and user feedback mechanisms
- Implement account linking for users with multiple login methods
- Add comprehensive logging and monitoring for authentication events