package middleware import ( "crypto/rand" "fmt" "math/big" "net/http" "time" "forge.cadoles.com/arcad/edge/pkg/jwtutil" "forge.cadoles.com/arcad/edge/pkg/module/auth" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) const AnonIssuer = "anon" func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler { opts := defaultAnonymousUserOptions() for _, fn := range funcs { fn(opts) } return func(next http.Handler) http.Handler { handler := func(w http.ResponseWriter, r *http.Request) { rawToken, err := jwtutil.FindRawToken(r, jwtutil.WithFinders( jwtutil.FindTokenFromAuthorizationHeader, jwtutil.FindTokenFromQueryString(auth.CookieName), jwtutil.FindTokenFromCookie(auth.CookieName), )) // If request already has a raw token, we do nothing if rawToken != "" && err == nil { next.ServeHTTP(w, r) return } ctx := r.Context() uuid, err := uuid.NewUUID() if err != nil { logger.Error(ctx, "could not generate uuid for anonymous user", logger.CapturedE(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String()) preferredUsername, err := generateRandomPreferredUsername(8) if err != nil { logger.Error(ctx, "could not generate preferred username for anonymous user", logger.CapturedE(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } claims := map[string]any{ auth.ClaimSubject: subject, auth.ClaimIssuer: AnonIssuer, auth.ClaimPreferredUsername: preferredUsername, auth.ClaimEdgeRole: opts.Role, auth.ClaimEdgeEntrypoint: opts.Entrypoint, auth.ClaimEdgeTenant: opts.Tenant, } token, err := jwtutil.SignedToken(key, signingAlgorithm, claims) if err != nil { logger.Error(ctx, "could not generate signed token", logger.CapturedE(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } cookieDomain, err := opts.GetCookieDomain(r) if err != nil { logger.Error(ctx, "could not retrieve cookie domain", logger.CapturedE(errors.WithStack(err))) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } cookie := http.Cookie{ Name: auth.CookieName, Value: string(token), Domain: cookieDomain, HttpOnly: false, Expires: time.Now().Add(opts.CookieDuration), Path: "/", } http.SetCookie(w, &cookie) next.ServeHTTP(w, r) } return http.HandlerFunc(handler) } } func generateRandomPreferredUsername(size int) (string, error) { var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") max := big.NewInt(int64(len(letters))) b := make([]rune, size) for i := range b { idx, err := rand.Int(rand.Reader, max) if err != nil { return "", errors.WithStack(err) } b[i] = letters[idx.Int64()] } return fmt.Sprintf("Anon %s", string(b)), nil }