191 lines
6.9 KiB
Go

package handlers
import (
"fmt"
"net/http"
"os"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/logto-io/go/client"
)
// SessionStorage implements Logto's Storage interface using Gin sessions
// Avoid cookie-based sessions due to size limits; use memory-based sessions for demo
// In production, use Redis/MongoDB
type SessionStorage struct {
session sessions.Session
}
func (s *SessionStorage) GetItem(key string) string {
value := s.session.Get(key)
if value == nil {
return ""
}
str, ok := value.(string)
if !ok {
return ""
}
return str
}
func (s *SessionStorage) SetItem(key, value string) {
s.session.Set(key, value)
s.session.Save()
}
// getLogtoConfig returns Logto config from environment variables
func getLogtoConfig() *client.LogtoConfig {
return &client.LogtoConfig{
Endpoint: os.Getenv("LOGTO_ENDPOINT"),
AppId: os.Getenv("LOGTO_APP_ID"),
AppSecret: os.Getenv("LOGTO_APP_SECRET"),
}
}
// HomeHandler shows auth state and sign-in/sign-out links
// Note: The /service/auth endpoint is for debugging purposes only
func HomeHandler(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session})
var debugInfo string
debugInfo = "<h1>Logto Auth Debugging Page</h1>"
// Basic auth state
if logtoClient.IsAuthenticated() {
debugInfo += "<div style='color: green; font-weight: bold;'>Authentication Status: Logged In ✓</div>"
// Get ID Token claims
idTokenClaims, err := logtoClient.GetIdTokenClaims()
if err != nil {
debugInfo += "<div style='color: red;'>Error fetching ID token claims: " + err.Error() + "</div>"
} else {
debugInfo += "<h2>ID Token Claims</h2>"
debugInfo += "<pre style='background-color: #f5f5f5; padding: 10px; overflow: auto; max-height: 400px;'>"
// Display claims in a simpler format
debugInfo += "<table style='width: 100%; border-collapse: collapse;'>"
debugInfo += "<tr><th style='text-align: left; padding: 5px; border-bottom: 1px solid #ddd;'>Claim</th><th style='text-align: left; padding: 5px; border-bottom: 1px solid #ddd;'>Value</th></tr>"
// Display the raw claims directly without any checks that might cause linter errors
debugInfo += fmt.Sprintf("<tr><td>sub</td><td>%v</td></tr>", idTokenClaims.Sub)
debugInfo += fmt.Sprintf("<tr><td>name</td><td>%v</td></tr>", idTokenClaims.Name)
debugInfo += fmt.Sprintf("<tr><td>email</td><td>%v</td></tr>", idTokenClaims.Email)
debugInfo += fmt.Sprintf("<tr><td>issuer</td><td>%v</td></tr>", idTokenClaims.Iss)
debugInfo += fmt.Sprintf("<tr><td>audience</td><td>%v</td></tr>", idTokenClaims.Aud)
debugInfo += fmt.Sprintf("<tr><td>expires at</td><td>%v</td></tr>", idTokenClaims.Exp)
debugInfo += fmt.Sprintf("<tr><td>issued at</td><td>%v</td></tr>", idTokenClaims.Iat)
debugInfo += "</table>"
debugInfo += "</pre>"
}
// Try to get access token info
hasAccessToken := false
var accessTokenErr error
_, accessTokenErr = logtoClient.GetAccessToken("")
if accessTokenErr == nil {
hasAccessToken = true
}
debugInfo += "<h2>Access Token Information</h2>"
if hasAccessToken {
debugInfo += "<div><strong>Access Token:</strong> Present (not displayed for security)</div>"
} else {
debugInfo += "<div>No resource-specific access token available</div>"
}
// Session information
debugInfo += "<h2>Session Information</h2>"
debugInfo += "<pre style='background-color: #f5f5f5; padding: 10px;'>"
// Get all keys from the session
for _, key := range []string{"idToken", "accessToken", "refreshToken", "expiresAt"} {
value := session.Get(key)
debugInfo += "<div><strong>" + key + ":</strong> "
if value == nil {
debugInfo += "Not set"
} else if strValue, ok := value.(string); ok {
if len(strValue) > 100 {
debugInfo += strValue[:100] + "..."
} else {
debugInfo += strValue
}
} else {
debugInfo += "Set (non-string value)"
}
debugInfo += "</div>"
}
debugInfo += "</pre>"
} else {
debugInfo += "<div style='color: red; font-weight: bold;'>Authentication Status: Not Logged In ✗</div>"
debugInfo += "<div>Sign in to see detailed authentication information</div>"
}
// Config information (excluding secrets)
debugInfo += "<h2>Logto Configuration</h2>"
debugInfo += "<div><strong>Endpoint:</strong> " + os.Getenv("LOGTO_ENDPOINT") + "</div>"
debugInfo += "<div><strong>App ID:</strong> " + os.Getenv("LOGTO_APP_ID") + "</div>"
debugInfo += "<div><strong>Redirect URI:</strong> " + os.Getenv("LOGTO_REDIRECT_URI") + "</div>"
// Add links for authentication actions
debugInfo += "<h2>Authentication Actions</h2>"
debugInfo += `<div><a href="/service/auth/sign-in" style="display: inline-block; margin: 10px 0; padding: 8px 16px; background-color: #4285f4; color: white; text-decoration: none; border-radius: 4px;">Sign In</a></div>`
debugInfo += `<div><a href="/service/auth/sign-out" style="display: inline-block; margin: 10px 0; padding: 8px 16px; background-color: #f44336; color: white; text-decoration: none; border-radius: 4px;">Sign Out</a></div>`
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(debugInfo))
}
// SignInHandler starts the Logto sign-in flow
func SignInHandler(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session})
redirectUri := os.Getenv("LOGTO_REDIRECT_URI")
signInUri, err := logtoClient.SignIn(redirectUri)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}
ctx.Redirect(http.StatusTemporaryRedirect, signInUri)
}
// CallbackHandler handles the Logto sign-in callback
func CallbackHandler(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session})
err := logtoClient.HandleSignInCallback(ctx.Request)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}
// Redirect to the frontend page instead of the backend auth page
ctx.Redirect(http.StatusTemporaryRedirect, os.Getenv("BASE_URL"))
}
// SignOutHandler starts the Logto sign-out flow
func SignOutHandler(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session})
postSignOutRedirectUri := os.Getenv("LOGTO_POST_SIGN_OUT_REDIRECT_URI")
signOutUri, err := logtoClient.SignOut(postSignOutRedirectUri)
if err != nil {
ctx.String(http.StatusOK, err.Error())
return
}
ctx.Redirect(http.StatusTemporaryRedirect, signOutUri)
}
// UserIdTokenClaimsHandler returns the user's ID token claims as JSON
func UserIdTokenClaimsHandler(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(getLogtoConfig(), &SessionStorage{session: session})
idTokenClaims, err := logtoClient.GetIdTokenClaims()
if err != nil {
ctx.String(http.StatusOK, err.Error())
return
}
ctx.JSON(http.StatusOK, idTokenClaims)
}