165 lines
4.8 KiB
Go
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
|
|
})
|
|
}
|