238 lines
5.7 KiB
Go
238 lines
5.7 KiB
Go
package otjwt
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/MicahParks/keyfunc/v3"
|
|
authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
|
"github.com/OliveTin/OliveTin/internal/config"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
|
if cfg.AuthJwtCertsURL != "" {
|
|
return parseJwtTokenWithRemoteKey(cfg, jwtString)
|
|
}
|
|
|
|
if cfg.AuthJwtPubKeyPath != "" {
|
|
return parseJwtTokenWithLocalKey(cfg, jwtString)
|
|
}
|
|
|
|
if cfg.AuthJwtHmacSecret == "" {
|
|
return nil, errors.New("no JWT authentication method configured")
|
|
}
|
|
|
|
return parseJwtTokenWithHMAC(cfg, jwtString)
|
|
}
|
|
|
|
func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
|
|
token, err := parseJwtToken(cfg, jwtString)
|
|
|
|
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 parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token, error) {
|
|
err := initJwks(cfg)
|
|
|
|
if err != nil {
|
|
log.Errorf("jwt init JWKS failure: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
|
|
}
|
|
|
|
var (
|
|
pubKeyBytes []byte = nil
|
|
pubKey *rsa.PublicKey
|
|
loadedKeyPath string
|
|
|
|
jwksVerifier keyfunc.Keyfunc
|
|
jwksOnce sync.Once
|
|
jwksInitErr error
|
|
|
|
localKeyMutex sync.RWMutex
|
|
localKeyInitErr error
|
|
)
|
|
|
|
func initJwks(cfg *config.Config) error {
|
|
jwksOnce.Do(func() {
|
|
if cfg.AuthJwtCertsURL != "" {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
var err error
|
|
jwksVerifier, err = keyfunc.NewDefaultCtx(ctx, []string{
|
|
cfg.AuthJwtCertsURL,
|
|
})
|
|
|
|
if err != nil {
|
|
log.Errorf("Init JWKS Failure: %v", err)
|
|
jwksInitErr = err
|
|
}
|
|
}
|
|
})
|
|
return jwksInitErr
|
|
}
|
|
|
|
func loadPublicKeyFromFile(keyPath string) error {
|
|
keyBytes, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't read public key from file %s", keyPath)
|
|
}
|
|
|
|
parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(keyBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing public key object (from %s)", keyPath)
|
|
}
|
|
|
|
pubKeyBytes = keyBytes
|
|
pubKey = parsedKey
|
|
loadedKeyPath = keyPath
|
|
localKeyInitErr = nil
|
|
return nil
|
|
}
|
|
|
|
func isKeyLoadedForPath(keyPath string) bool {
|
|
return pubKeyBytes != nil && loadedKeyPath == keyPath
|
|
}
|
|
|
|
func readLocalPublicKeyWithLock(keyPath string) error {
|
|
localKeyMutex.RLock()
|
|
alreadyLoaded := isKeyLoadedForPath(keyPath)
|
|
localKeyMutex.RUnlock()
|
|
|
|
if alreadyLoaded {
|
|
return nil
|
|
}
|
|
|
|
localKeyMutex.Lock()
|
|
defer localKeyMutex.Unlock()
|
|
|
|
if isKeyLoadedForPath(keyPath) {
|
|
return nil
|
|
}
|
|
|
|
localKeyInitErr = loadPublicKeyFromFile(keyPath)
|
|
return localKeyInitErr
|
|
}
|
|
|
|
func readLocalPublicKey(cfg *config.Config) error {
|
|
if cfg.AuthJwtPubKeyPath == "" {
|
|
return errors.New("no JWT public key path configured")
|
|
}
|
|
return readLocalPublicKeyWithLock(cfg.AuthJwtPubKeyPath)
|
|
}
|
|
|
|
func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
|
err := readLocalPublicKey(cfg)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
|
return nil, fmt.Errorf("parseJwt expected token algorithm RSA but got: %v", token.Header["alg"])
|
|
}
|
|
|
|
return pubKey, nil
|
|
})
|
|
}
|
|
|
|
// Hash-based Message Authentication Code
|
|
func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
|
return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("parseJwt expected token algorithm HMAC but got: %v", token.Header["alg"])
|
|
}
|
|
|
|
return []byte(cfg.AuthJwtHmacSecret), nil
|
|
})
|
|
}
|
|
|
|
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 CheckUserFromJwtCookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
|
cookie, err := context.Request.Cookie(context.Config.AuthJwtCookieName)
|
|
|
|
if err != nil {
|
|
log.Debugf("jwt cookie check %v name: %v", err, context.Config.AuthJwtCookieName)
|
|
return nil
|
|
}
|
|
|
|
return parseJwt(context.Config, cookie.Value)
|
|
}
|
|
|
|
func CheckUserFromJwtHeader(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
|
header := context.Request.Header.Get(context.Config.AuthJwtHeader)
|
|
if header == "" {
|
|
return nil
|
|
}
|
|
|
|
token := strings.TrimPrefix(header, "Bearer ")
|
|
token = strings.TrimSpace(token)
|
|
|
|
return parseJwt(context.Config, token)
|
|
}
|
|
|
|
func parseJwt(cfg *config.Config, token string) *authTypes.AuthenticatedUser {
|
|
claims, err := getClaimsFromJwtToken(cfg, token)
|
|
|
|
if err != nil {
|
|
log.Warnf("jwt claim error: %+v", err)
|
|
return nil
|
|
}
|
|
|
|
if cfg.InsecureAllowDumpJwtClaims {
|
|
log.Debugf("JWT Claims %+v", claims)
|
|
}
|
|
|
|
user := &authTypes.AuthenticatedUser{
|
|
Username: lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, ""),
|
|
UsergroupLine: parseGroupClaim(cfg.AuthJwtClaimUserGroup, claims),
|
|
Provider: "jwt",
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
func parseGroupClaim(groupClaim string, claims jwt.MapClaims) string {
|
|
usergroup := ""
|
|
if val, ok := claims[groupClaim]; ok {
|
|
if array, ok := val.([]interface{}); ok {
|
|
groups := make([]string, len(array))
|
|
for i, v := range array {
|
|
groups[i] = fmt.Sprintf("%s", v)
|
|
}
|
|
usergroup = strings.Join(groups, " ")
|
|
} else {
|
|
usergroup = fmt.Sprintf("%s", val)
|
|
}
|
|
}
|
|
return usergroup
|
|
}
|