feat: add Go current-session HTTP adapter
This commit is contained in:
1 parent
c849077591
commit
772795e7eb
10 files changed
+1608
-7
No files matched your search
@@ -0,0 +1,164 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user