diff --git a/internal/config/config.go b/internal/config/config.go index e9b5f0b..cd09819 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -76,6 +76,8 @@ type Config struct { AuthJwtSecret string AuthJwtClaimUsername string AuthJwtClaimUserGroup string + AuthHttpHeaderUsername string + AuthHttpHeaderUserGroup string DefaultPermissions PermissionsList AccessControlLists []AccessControlList } diff --git a/internal/httpservers/restapi.go b/internal/httpservers/restapi.go index fa3cf78..f8f2fbf 100644 --- a/internal/httpservers/restapi.go +++ b/internal/httpservers/restapi.go @@ -2,8 +2,6 @@ package httpservers import ( "context" - "errors" - "fmt" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" log "github.com/sirupsen/logrus" "google.golang.org/grpc" @@ -11,8 +9,6 @@ import ( "google.golang.org/protobuf/encoding/protojson" "net/http" - "github.com/golang-jwt/jwt/v4" - gw "github.com/OliveTin/OliveTin/gen/grpc" config "github.com/OliveTin/OliveTin/internal/config" @@ -23,39 +19,44 @@ var ( cfg *config.Config ) -func parseToken(cookieValue string) (*jwt.Token, error) { - return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) { - // Don't forget to validate the alg is what you expect: - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) +func parseHttpHeaderForAuth(req *http.Request) (string, string) { + username, ok := req.Header[cfg.AuthHttpHeaderUsername] + + if !ok { + return "", "" + } + + if cfg.AuthHttpHeaderUserGroup != "" { + usergroup, ok := req.Header[cfg.AuthHttpHeaderUserGroup] + + if ok { + return username[0], usergroup[0] } + } - // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") - return []byte(cfg.AuthJwtSecret), nil - }) + return username[0], "" } -func getClaimsFromJwtToken(cookieValue string) (jwt.MapClaims, error) { - token, err := parseToken(cookieValue) +func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD { + username := "" + usergroup := "" - if err != nil { - log.Errorf("jwt parse failure: %v", err) - return nil, errors.New("jwt parse failure") + if cfg.AuthJwtCookieName != "" { + username, usergroup = parseJwtCookie(req) } - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - return claims, nil - } else { - return nil, errors.New("jwt token isn't valid") + if cfg.AuthHttpHeaderUsername != "" { + username, usergroup = parseHttpHeaderForAuth(req) } -} -func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string { - if val, ok := claims[key]; ok { - return fmt.Sprintf("%s", val) - } else { - return def - } + md := metadata.Pairs( + "username", username, + "usergroup", usergroup, + ) + + log.Debugf("jwt usable claims: %+v", md) + + return md } func startRestAPIServer(globalConfig *config.Config) error { @@ -72,33 +73,7 @@ func startRestAPIServer(globalConfig *config.Config) error { // The JSONPb.EmitDefaults is necssary, so "empty" fields are returned in JSON. mux := runtime.NewServeMux( runtime.WithMetadata(func(ctx context.Context, request *http.Request) metadata.MD { - cookie, err := request.Cookie(cfg.AuthJwtCookieName) - - if err != nil { - log.Debugf("jwt cookie check %v name: %v", err, cfg.AuthJwtCookieName) - return nil - } - - claims, err := getClaimsFromJwtToken(cookie.Value) - - log.Debugf("jwt claims data: %+v", claims) - - if err != nil { - log.Warnf("jwt claim error: %+v", err) - return nil - } - - username := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, "none") - usergroup := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUserGroup, "none") - - md := metadata.Pairs( - "username", username, - "usergroup", usergroup, - ) - - log.Debugf("jwt usable claims: %+v", md) - - return md + return parseRequestMetadata(ctx, request) }), runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ Marshaler: &runtime.JSONPb{ diff --git a/internal/httpservers/restapi_auth_jwt.go b/internal/httpservers/restapi_auth_jwt.go new file mode 100644 index 0000000..5a6baf4 --- /dev/null +++ b/internal/httpservers/restapi_auth_jwt.go @@ -0,0 +1,68 @@ +package httpservers; + +import ( + "github.com/golang-jwt/jwt/v4" + "fmt" + log "github.com/sirupsen/logrus" + "errors" + "net/http" +) + +func parseJwtToken(cookieValue string) (*jwt.Token, error) { + return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") + return []byte(cfg.AuthJwtSecret), nil + }) +} + +func getClaimsFromJwtToken(cookieValue string) (jwt.MapClaims, error) { + token, err := parseJwtToken(cookieValue) + + if err != nil { + log.Errorf("jwt parse failure: %v", err) + return nil, errors.New("jwt parse failure") + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil + } else { + return nil, errors.New("jwt token isn't valid") + } +} + +func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string { + if val, ok := claims[key]; ok { + return fmt.Sprintf("%s", val) + } else { + return def + } +} + +func parseJwtCookie(request *http.Request) (string, string) { + cookie, err := request.Cookie(cfg.AuthJwtCookieName) + + if err != nil { + log.Debugf("jwt cookie check %v name: %v", err, cfg.AuthJwtCookieName) + return "", "" + } + + claims, err := getClaimsFromJwtToken(cookie.Value) + + log.Debugf("jwt claims data: %+v", claims) + + if err != nil { + log.Warnf("jwt claim error: %+v", err) + return "", "" + } + + username := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, "") + usergroup := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUserGroup, "") + + return username, usergroup +} +