feat: generalize defaut user middleware

This commit is contained in:
wpetit 2024-05-02 09:42:56 +02:00
parent 6aec6da078
commit 0f673671b8
4 changed files with 149 additions and 106 deletions

View File

@ -252,7 +252,7 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN,
fetchModule.Mount(), fetchModule.Mount(),
), ),
appHTTP.WithHTTPMiddlewares( appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(key, jwa.HS256), authModuleMiddleware.DefaultUser(key, jwa.HS256, authModuleMiddleware.WithAnonymousUser()),
), ),
) )
if err := handler.Load(ctx, bundle); err != nil { if err := handler.Load(ctx, bundle); err != nil {

View File

@ -5,99 +5,43 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"net/http" "net/http"
"time"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
) )
const AnonIssuer = "anon" const AnonIssuer = "anon"
func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler { func WithAnonymousUser(funcs ...DefaultUserOptionFunc) DefaultUserOptionFunc {
opts := defaultAnonymousUserOptions() return func(opts *DefaultUserOptions) {
opts.GetSubject = getAnonymousSubject
opts.GetPreferredUsername = getAnonymousPreferredUsername
opts.Issuer = AnonIssuer
for _, fn := range funcs { for _, fn := range funcs {
fn(opts) 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() func getAnonymousSubject(r *http.Request) (string, error) {
uuid, err := uuid.NewUUID() uuid, err := uuid.NewUUID()
if err != nil { if err != nil {
logger.Error(ctx, "could not generate uuid for anonymous user", logger.CapturedE(errors.WithStack(err))) return "", errors.Wrap(err, "could not generate uuid for anonymous user")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} }
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String()) subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
return subject, nil
}
func getAnonymousPreferredUsername(r *http.Request) (string, error) {
preferredUsername, err := generateRandomPreferredUsername(8) preferredUsername, err := generateRandomPreferredUsername(8)
if err != nil { if err != nil {
logger.Error(ctx, "could not generate preferred username for anonymous user", logger.CapturedE(errors.WithStack(err))) return "", errors.Wrap(err, "could not generate preferred username for anonymous user")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
} }
claims := map[string]any{ return preferredUsername, nil
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) { func generateRandomPreferredUsername(size int) (string, error) {

View File

@ -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)
}
}

View File

@ -11,47 +11,52 @@ func defaultGetCookieDomain(r *http.Request) (string, error) {
return "", nil return "", nil
} }
type AnonymousUserOptions struct { type DefaultUserOptions struct {
GetCookieDomain GetCookieDomainFunc GetCookieDomain GetCookieDomainFunc
CookieDuration time.Duration CookieDuration time.Duration
Tenant string Tenant string
Entrypoint string Entrypoint string
Role 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 { func defaultUserOptions() *DefaultUserOptions {
return &AnonymousUserOptions{ return &DefaultUserOptions{
GetCookieDomain: defaultGetCookieDomain, GetCookieDomain: defaultGetCookieDomain,
CookieDuration: 24 * time.Hour, CookieDuration: 24 * time.Hour,
Tenant: "", Tenant: "",
Entrypoint: "", Entrypoint: "",
Role: "", Role: "",
GetSubject: getAnonymousSubject,
GetPreferredUsername: getAnonymousPreferredUsername,
} }
} }
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc { func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.GetCookieDomain = getCookieDomain opts.GetCookieDomain = getCookieDomain
opts.CookieDuration = duration opts.CookieDuration = duration
} }
} }
func WithTenant(tenant string) AnonymousUserOptionFunc { func WithTenant(tenant string) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.Tenant = tenant opts.Tenant = tenant
} }
} }
func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc { func WithEntrypoint(entrypoint string) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.Entrypoint = entrypoint opts.Entrypoint = entrypoint
} }
} }
func WithRole(role string) AnonymousUserOptionFunc { func WithRole(role string) DefaultUserOptionFunc {
return func(opts *AnonymousUserOptions) { return func(opts *DefaultUserOptions) {
opts.Role = role opts.Role = role
} }
} }