191 lines
6.9 KiB
Go
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)
|
|
}
|