feat: generalize defaut user middleware
This commit is contained in:
parent
6aec6da078
commit
0f673671b8
|
@ -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 {
|
||||||
|
|
|
@ -5,101 +5,45 @@ 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) {
|
||||||
for _, fn := range funcs {
|
opts.GetSubject = getAnonymousSubject
|
||||||
fn(opts)
|
opts.GetPreferredUsername = getAnonymousPreferredUsername
|
||||||
}
|
opts.Issuer = AnonIssuer
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
for _, fn := range funcs {
|
||||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
fn(opts)
|
||||||
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 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) {
|
func generateRandomPreferredUsername(size int) (string, error) {
|
||||||
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
max := big.NewInt(int64(len(letters)))
|
max := big.NewInt(int64(len(letters)))
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue