Files

165 lines
4.8 KiB
Go

package httpapi
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
)
// AuthState is the runtime authentication configuration exposed by auth/me.
type AuthState struct {
Required bool
Configured bool
}
// SessionResolver is the HTTP module's consumer-owned seam to Identity.
type SessionResolver interface {
Resolve(context.Context, string) (identity.Session, error)
}
type authMeHandler struct {
state AuthState
resolver SessionResolver
}
// NewAuthMeHandler returns the standalone current-session HTTP adapter.
func NewAuthMeHandler(state AuthState, resolver SessionResolver) (http.Handler, error) {
if state.Configured && resolver == nil {
return nil, fmt.Errorf("auth/me: configured authentication requires a session resolver")
}
return &authMeHandler{state: state, resolver: resolver}, nil
}
func (handler *authMeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/auth/me" {
w.WriteHeader(http.StatusNotFound)
return
}
switch r.Method {
case http.MethodOptions:
w.Header().Set("Allow", "GET, HEAD, OPTIONS")
w.WriteHeader(http.StatusNoContent)
return
case http.MethodGet, http.MethodHead:
// Continue below: HEAD deliberately executes the same authentication
// work as GET and suppresses only the representation body.
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
response, err := handler.currentSession(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
payload, err := json.Marshal(response)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodGet {
_, _ = w.Write(payload)
}
}
type authMeResponse struct {
Authenticated bool `json:"authenticated"`
AuthRequired bool `json:"authRequired"`
AuthConfigured bool `json:"authConfigured"`
AuthMode *identity.AuthMode `json:"authMode"`
User *publicUser `json:"user"`
}
type publicUser struct {
ID string `json:"id"`
Subject string `json:"subject"`
Username string `json:"username,omitempty"`
Phone string `json:"phone,omitempty"`
DisplayName string `json:"displayName"`
ClientID string `json:"clientId"`
TenantID string `json:"tenantId,omitempty"`
OrganizationID string `json:"organizationId,omitempty"`
OrganizationName string `json:"organizationName,omitempty"`
Role string `json:"role,omitempty"`
Status string `json:"status,omitempty"`
Authorities []string `json:"authorities"`
Scope []string `json:"scope"`
}
func (handler *authMeHandler) currentSession(r *http.Request) (authMeResponse, error) {
response := authMeResponse{
AuthRequired: handler.state.Required,
AuthConfigured: handler.state.Configured,
}
if !handler.state.Configured {
return response, nil
}
cookieValue, ok := readSessionCookie(r)
if !ok || len(cookieValue) > identity.CookieMaxValueLength {
return response, nil
}
session, err := handler.resolver.Resolve(r.Context(), cookieValue)
if errors.Is(err, identity.ErrUnauthenticated) {
return response, nil
}
if err != nil {
return authMeResponse{}, err
}
authorities := session.User.Authorities
if authorities == nil {
authorities = []string{}
}
scope := session.User.Scope
if scope == nil {
scope = []string{}
}
response.Authenticated = true
response.AuthMode = &session.AuthMode
response.User = &publicUser{
ID: session.User.ID,
Subject: session.User.Subject,
Username: session.User.Username,
Phone: session.User.Phone,
DisplayName: session.User.DisplayName,
ClientID: session.User.ClientID,
TenantID: session.User.TenantID,
OrganizationID: session.User.OrganizationID,
OrganizationName: session.User.OrganizationName,
Role: session.User.Role,
Status: session.User.Status,
Authorities: authorities,
Scope: scope,
}
return response, nil
}
func readSessionCookie(r *http.Request) (string, bool) {
protected := make(map[string]struct{}, identity.CookieMaxChunks)
for _, name := range identity.CookieNames() {
protected[name] = struct{}{}
}
values := make(map[string]string, identity.CookieMaxChunks)
for _, cookie := range r.Cookies() {
if _, ok := protected[cookie.Name]; !ok {
continue
}
// Match the pinned Next.js RequestCookies parser: the final value for a
// duplicated name wins. net/http's Request.Cookie would choose the first.
values[cookie.Name] = cookie.Value
}
return identity.Reassemble(identity.SessionCookieName, func(name string) (string, bool) {
value, ok := values[name]
return value, ok
})
}