diff --git a/cmd/cli/command/app/run.go b/cmd/cli/command/app/run.go index 699a07d..b628c84 100644 --- a/cmd/cli/command/app/run.go +++ b/cmd/cli/command/app/run.go @@ -252,7 +252,7 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN, fetchModule.Mount(), ), appHTTP.WithHTTPMiddlewares( - authModuleMiddleware.AnonymousUser(key, jwa.HS256), + authModuleMiddleware.DefaultUser(key, jwa.HS256, authModuleMiddleware.WithAnonymousUser()), ), ) if err := handler.Load(ctx, bundle); err != nil { diff --git a/pkg/module/auth/middleware/anonymous_user.go b/pkg/module/auth/middleware/anonymous_user.go index 92caa3a..fa49eb6 100644 --- a/pkg/module/auth/middleware/anonymous_user.go +++ b/pkg/module/auth/middleware/anonymous_user.go @@ -5,101 +5,45 @@ import ( "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) - } +func WithAnonymousUser(funcs ...DefaultUserOptionFunc) DefaultUserOptionFunc { + return func(opts *DefaultUserOptions) { + opts.GetSubject = getAnonymousSubject + opts.GetPreferredUsername = getAnonymousPreferredUsername + opts.Issuer = AnonIssuer - 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) + for _, fn := range funcs { + fn(opts) } - - return http.HandlerFunc(handler) } } +func getAnonymousSubject(r *http.Request) (string, error) { + uuid, err := uuid.NewUUID() + if err != nil { + return "", errors.Wrap(err, "could not generate uuid for anonymous user") + } + + subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String()) + + return subject, nil +} + +func getAnonymousPreferredUsername(r *http.Request) (string, error) { + preferredUsername, err := generateRandomPreferredUsername(8) + if err != nil { + return "", errors.Wrap(err, "could not generate preferred username for anonymous user") + } + + return preferredUsername, nil +} + func generateRandomPreferredUsername(size int) (string, error) { var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") max := big.NewInt(int64(len(letters))) diff --git a/pkg/module/auth/middleware/default_user.go b/pkg/module/auth/middleware/default_user.go new file mode 100644 index 0000000..dfd1e16 --- /dev/null +++ b/pkg/module/auth/middleware/default_user.go @@ -0,0 +1,94 @@ +package middleware + +import ( + "net/http" + "time" + + "forge.cadoles.com/arcad/edge/pkg/jwtutil" + "forge.cadoles.com/arcad/edge/pkg/module/auth" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func DefaultUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...DefaultUserOptionFunc) func(next http.Handler) http.Handler { + opts := defaultUserOptions() + 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() + + subject, err := opts.GetSubject(r) + if err != nil { + logger.Error(ctx, "could not retrieve user subject", logger.CapturedE(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + preferredUsername, err := opts.GetPreferredUsername(r) + if err != nil { + logger.Error(ctx, "could not retrieve user preferred username", logger.CapturedE(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + claims := map[string]any{ + auth.ClaimSubject: subject, + auth.ClaimIssuer: opts.Issuer, + 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) + } +} diff --git a/pkg/module/auth/middleware/options.go b/pkg/module/auth/middleware/options.go index ccefa3b..6162886 100644 --- a/pkg/module/auth/middleware/options.go +++ b/pkg/module/auth/middleware/options.go @@ -11,47 +11,52 @@ func defaultGetCookieDomain(r *http.Request) (string, error) { return "", nil } -type AnonymousUserOptions struct { - GetCookieDomain GetCookieDomainFunc - CookieDuration time.Duration - Tenant string - Entrypoint string - Role string +type DefaultUserOptions struct { + GetCookieDomain GetCookieDomainFunc + CookieDuration time.Duration + Tenant string + Entrypoint string + Role string + Issuer string + GetPreferredUsername func(r *http.Request) (string, error) + GetSubject func(r *http.Request) (string, error) } -type AnonymousUserOptionFunc func(*AnonymousUserOptions) +type DefaultUserOptionFunc func(opts *DefaultUserOptions) -func defaultAnonymousUserOptions() *AnonymousUserOptions { - return &AnonymousUserOptions{ - GetCookieDomain: defaultGetCookieDomain, - CookieDuration: 24 * time.Hour, - Tenant: "", - Entrypoint: "", - Role: "", +func defaultUserOptions() *DefaultUserOptions { + return &DefaultUserOptions{ + GetCookieDomain: defaultGetCookieDomain, + CookieDuration: 24 * time.Hour, + Tenant: "", + Entrypoint: "", + Role: "", + GetSubject: getAnonymousSubject, + GetPreferredUsername: getAnonymousPreferredUsername, } } -func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc { - return func(opts *AnonymousUserOptions) { +func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) DefaultUserOptionFunc { + return func(opts *DefaultUserOptions) { opts.GetCookieDomain = getCookieDomain opts.CookieDuration = duration } } -func WithTenant(tenant string) AnonymousUserOptionFunc { - return func(opts *AnonymousUserOptions) { +func WithTenant(tenant string) DefaultUserOptionFunc { + return func(opts *DefaultUserOptions) { opts.Tenant = tenant } } -func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc { - return func(opts *AnonymousUserOptions) { +func WithEntrypoint(entrypoint string) DefaultUserOptionFunc { + return func(opts *DefaultUserOptions) { opts.Entrypoint = entrypoint } } -func WithRole(role string) AnonymousUserOptionFunc { - return func(opts *AnonymousUserOptions) { +func WithRole(role string) DefaultUserOptionFunc { + return func(opts *DefaultUserOptions) { opts.Role = role } }