Merge branch '189-refactor-authentication-module' of https://github.com/ClusterCockpit/cc-backend into 189-refactor-authentication-module

This commit is contained in:
Christoph Kluge 2023-08-18 11:17:33 +02:00
commit 734e818b19
8 changed files with 65 additions and 74 deletions

View File

@ -211,10 +211,7 @@ func main() {
var authentication *auth.Authentication
if !config.Keys.DisableAuthentication {
var err error
if authentication, err = auth.Init(map[string]interface{}{
"ldap": config.Keys.LdapConfig,
"jwt": config.Keys.JwtConfig,
}); err != nil {
if authentication, err = auth.Init(); err != nil {
log.Fatalf("auth initialization failed: %v", err)
}

View File

@ -14,6 +14,7 @@ import (
"os"
"time"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -21,7 +22,6 @@ import (
)
type Authenticator interface {
Init(config interface{}) error
CanLogin(user *schema.User, username string, rw http.ResponseWriter, r *http.Request) (*schema.User, bool)
Login(user *schema.User, rw http.ResponseWriter, r *http.Request) (*schema.User, error)
}
@ -80,7 +80,7 @@ func (auth *Authentication) AuthViaSession(
}, nil
}
func Init(configs map[string]interface{}) (*Authentication, error) {
func Init() (*Authentication, error) {
auth := &Authentication{}
sessKey := os.Getenv("SESSION_KEY")
@ -101,9 +101,9 @@ func Init(configs map[string]interface{}) (*Authentication, error) {
auth.sessionStore = sessions.NewCookieStore(bytes)
}
if config, ok := configs["ldap"]; ok {
if config.Keys.LdapConfig != nil {
ldapAuth := &LdapAuthenticator{}
if err := ldapAuth.Init(config); err != nil {
if err := ldapAuth.Init(); err != nil {
log.Warn("Error while initializing authentication -> ldapAuth init failed")
} else {
auth.LdapAuth = ldapAuth
@ -113,32 +113,32 @@ func Init(configs map[string]interface{}) (*Authentication, error) {
log.Info("Missing LDAP configuration: No LDAP support!")
}
if config, ok := configs["jwt"]; ok {
if config.Keys.JwtConfig != nil {
auth.JwtAuth = &JWTAuthenticator{}
if err := auth.JwtAuth.Init(config); err != nil {
if err := auth.JwtAuth.Init(); err != nil {
log.Error("Error while initializing authentication -> jwtAuth init failed")
return nil, err
}
jwtSessionAuth := &JWTSessionAuthenticator{}
if err := jwtSessionAuth.Init(config); err != nil {
log.Warn("Error while initializing authentication -> jwtSessionAuth init failed")
if err := jwtSessionAuth.Init(); err != nil {
log.Info("jwtSessionAuth init failed: No JWT login support!")
} else {
auth.authenticators = append(auth.authenticators, jwtSessionAuth)
}
jwtCookieSessionAuth := &JWTCookieSessionAuthenticator{}
if err := jwtCookieSessionAuth.Init(configs["jwt"]); err != nil {
log.Warn("Error while initializing authentication -> jwtCookieSessionAuth init failed")
if err := jwtCookieSessionAuth.Init(); err != nil {
log.Info("jwtCookieSessionAuth init failed: No JWT cookie login support!")
} else {
auth.authenticators = append(auth.authenticators, jwtCookieSessionAuth)
}
} else {
log.Info("Missing JWT configuration: No JWT token login support!")
log.Info("Missing JWT configuration: No JWT token support!")
}
auth.LocalAuth = &LocalAuthenticator{}
if err := auth.LocalAuth.Init(nil); err != nil {
if err := auth.LocalAuth.Init(); err != nil {
log.Error("Error while initializing authentication -> localAuth init failed")
return nil, err
}
@ -152,11 +152,11 @@ func (auth *Authentication) Login(
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error)) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
err := errors.New("no authenticator applied")
username := r.FormValue("username")
var dbUser *schema.User
if username != "" {
var err error
dbUser, err = repository.GetUserRepository().GetUser(username)
if err != nil && err != sql.ErrNoRows {
log.Errorf("Error while loading user '%v'", username)
@ -170,7 +170,7 @@ func (auth *Authentication) Login(
continue
}
user, err = authenticator.Login(user, rw, r)
user, err := authenticator.Login(user, rw, r)
if err != nil {
log.Warnf("user login failed: %s", err.Error())
onfailure(rw, r, err)
@ -203,7 +203,7 @@ func (auth *Authentication) Login(
}
log.Debugf("login failed: no authenticator applied")
onfailure(rw, r, err)
onfailure(rw, r, errors.New("no authenticator applied"))
})
}

View File

@ -13,6 +13,7 @@ import (
"strings"
"time"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -22,12 +23,9 @@ import (
type JWTAuthenticator struct {
publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey
config *schema.JWTAuthConfig
}
func (ja *JWTAuthenticator) Init(conf interface{}) error {
ja.config = conf.(*schema.JWTAuthConfig)
func (ja *JWTAuthenticator) Init() error {
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
if pubKey == "" || privKey == "" {
log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
@ -87,7 +85,7 @@ func (ja *JWTAuthenticator) AuthViaJWT(
var roles []string
// Validate user + roles from JWT against database?
if ja.config != nil && ja.config.ValidateUser {
if config.Keys.JwtConfig.ValidateUser {
ur := repository.GetUserRepository()
user, err := ur.GetUser(sub)
@ -130,8 +128,8 @@ func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) {
"roles": user.Roles,
"iat": now.Unix(),
}
if ja.config != nil && ja.config.MaxAge != "" {
d, err := time.ParseDuration(ja.config.MaxAge)
if config.Keys.JwtConfig.MaxAge != "" {
d, err := time.ParseDuration(config.Keys.JwtConfig.MaxAge)
if err != nil {
return "", errors.New("cannot parse max-age config key")
}

View File

@ -12,6 +12,7 @@ import (
"net/http"
"os"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -22,15 +23,11 @@ type JWTCookieSessionAuthenticator struct {
publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey
publicKeyCrossLogin ed25519.PublicKey // For accepting externally generated JWTs
config *schema.JWTAuthConfig
}
var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil)
func (ja *JWTCookieSessionAuthenticator) Init(conf interface{}) error {
ja.config = conf.(*schema.JWTAuthConfig)
func (ja *JWTCookieSessionAuthenticator) Init() error {
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
if pubKey == "" || privKey == "" {
log.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
@ -65,17 +62,18 @@ func (ja *JWTCookieSessionAuthenticator) Init(conf interface{}) error {
return errors.New("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
}
jc := config.Keys.JwtConfig
// Warn if other necessary settings are not configured
if ja.config != nil {
if ja.config.CookieName == "" {
log.Warn("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
if jc != nil {
if jc.CookieName == "" {
log.Info("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
return errors.New("cookieName for JWTs not configured (cross login via JWT cookie will fail)")
}
if !ja.config.ValidateUser {
log.Warn("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
if !jc.ValidateUser {
log.Info("forceJWTValidationViaDatabase not set to true: CC will accept users and roles defined in JWTs regardless of its own database!")
}
if ja.config.TrustedIssuer == "" {
log.Warn("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
if jc.TrustedIssuer == "" {
log.Info("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
return errors.New("trustedExternalIssuer for JWTs not configured (cross login via JWT cookie will fail)")
}
} else {
@ -93,9 +91,10 @@ func (ja *JWTCookieSessionAuthenticator) CanLogin(
rw http.ResponseWriter,
r *http.Request) (*schema.User, bool) {
jc := config.Keys.JwtConfig
cookieName := ""
if ja.config != nil && ja.config.CookieName != "" {
cookieName = ja.config.CookieName
if jc.CookieName != "" {
cookieName = jc.CookieName
}
// Try to read the JWT cookie
@ -115,7 +114,8 @@ func (ja *JWTCookieSessionAuthenticator) Login(
rw http.ResponseWriter,
r *http.Request) (*schema.User, error) {
jwtCookie, err := r.Cookie(ja.config.CookieName)
jc := config.Keys.JwtConfig
jwtCookie, err := r.Cookie(jc.CookieName)
var rawtoken string
if err == nil && jwtCookie.Value != "" {
@ -128,7 +128,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
}
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
if success && unvalidatedIssuer == ja.config.TrustedIssuer {
if success && unvalidatedIssuer == jc.TrustedIssuer {
// The (unvalidated) issuer seems to be the expected one,
// use public cross login key from config
return ja.publicKeyCrossLogin, nil
@ -167,7 +167,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
var roles []string
if ja.config.ValidateUser {
if jc.ValidateUser {
// Deny any logins for unknown usernames
if user == nil {
log.Warn("Could not find user from JWT in internal database.")
@ -189,7 +189,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
// (Ask browser to) Delete JWT cookie
deletedCookie := &http.Cookie{
Name: ja.config.CookieName,
Name: jc.CookieName,
Value: "",
Path: "/",
MaxAge: -1,
@ -208,7 +208,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
AuthSource: schema.AuthViaToken,
}
if ja.config.SyncUserOnLogin {
if jc.SyncUserOnLogin {
if err := repository.GetUserRepository().AddUser(user); err != nil {
log.Errorf("Error while adding user '%s' to DB", user.Username)
}

View File

@ -12,6 +12,7 @@ import (
"os"
"strings"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -20,15 +21,11 @@ import (
type JWTSessionAuthenticator struct {
loginTokenKey []byte // HS256 key
config *schema.JWTAuthConfig
}
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
func (ja *JWTSessionAuthenticator) Init(conf interface{}) error {
ja.config = conf.(*schema.JWTAuthConfig)
func (ja *JWTSessionAuthenticator) Init() error {
if pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY"); pubKey != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil {
@ -96,7 +93,7 @@ func (ja *JWTSessionAuthenticator) Login(
var roles []string
if ja.config.ValidateUser {
if config.Keys.JwtConfig.ValidateUser {
// Deny any logins for unknown usernames
if user == nil {
log.Warn("Could not find user from JWT in internal database.")
@ -142,7 +139,7 @@ func (ja *JWTSessionAuthenticator) Login(
AuthSource: schema.AuthViaToken,
}
if ja.config.SyncUserOnLogin {
if config.Keys.JwtConfig.SyncUserOnLogin {
if err := repository.GetUserRepository().AddUser(user); err != nil {
log.Errorf("Error while adding user '%s' to DB", user.Username)
}

View File

@ -12,6 +12,7 @@ import (
"strings"
"time"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -19,25 +20,22 @@ import (
)
type LdapAuthenticator struct {
config *schema.LdapConfig
syncPassword string
}
var _ Authenticator = (*LdapAuthenticator)(nil)
func (la *LdapAuthenticator) Init(conf interface{}) error {
la.config = conf.(*schema.LdapConfig)
func (la *LdapAuthenticator) Init() error {
la.syncPassword = os.Getenv("LDAP_ADMIN_PASSWORD")
if la.syncPassword == "" {
log.Warn("environment variable 'LDAP_ADMIN_PASSWORD' not set (ldap sync will not work)")
}
if la.config != nil && la.config.SyncInterval != "" {
interval, err := time.ParseDuration(la.config.SyncInterval)
if config.Keys.LdapConfig.SyncInterval != "" {
interval, err := time.ParseDuration(config.Keys.LdapConfig.SyncInterval)
if err != nil {
log.Warnf("Could not parse duration for sync interval: %v", la.config.SyncInterval)
log.Warnf("Could not parse duration for sync interval: %v",
config.Keys.LdapConfig.SyncInterval)
return err
}
@ -57,7 +55,7 @@ func (la *LdapAuthenticator) Init(conf interface{}) error {
}
}()
} else {
log.Info("Missing LDAP configuration key sync_interval")
log.Info("LDAP configuration key sync_interval invalid")
}
return nil
@ -69,12 +67,14 @@ func (la *LdapAuthenticator) CanLogin(
rw http.ResponseWriter,
r *http.Request) (*schema.User, bool) {
lc := config.Keys.LdapConfig
if user != nil {
if user.AuthSource == schema.AuthViaLDAP {
return user, true
}
} else {
if la.config.SyncUserOnLogin {
if lc.SyncUserOnLogin {
l, err := la.getLdapConnection(true)
if err != nil {
log.Error("LDAP connection error")
@ -83,9 +83,9 @@ func (la *LdapAuthenticator) CanLogin(
// Search for the given username
searchRequest := ldap.NewSearchRequest(
la.config.UserBase,
lc.UserBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&%s(uid=%s))", la.config.UserFilter, username),
fmt.Sprintf("(&%s(uid=%s))", lc.UserFilter, username),
[]string{"dn", "uid", "gecos"}, nil)
sr, err := l.Search(searchRequest)
@ -138,7 +138,7 @@ func (la *LdapAuthenticator) Login(
}
defer l.Close()
userDn := strings.Replace(la.config.UserBind, "{username}", user.Username, -1)
userDn := strings.Replace(config.Keys.LdapConfig.UserBind, "{username}", user.Username, -1)
if err := l.Bind(userDn, r.FormValue("password")); err != nil {
log.Errorf("AUTH/LDAP > Authentication for user %s failed: %v",
user.Username, err)
@ -153,6 +153,7 @@ func (la *LdapAuthenticator) Sync() error {
const IN_LDAP int = 2
const IN_BOTH int = 3
ur := repository.GetUserRepository()
lc := config.Keys.LdapConfig
users := map[string]int{}
usernames, err := ur.GetLdapUsernames()
@ -172,9 +173,9 @@ func (la *LdapAuthenticator) Sync() error {
defer l.Close()
ldapResults, err := l.Search(ldap.NewSearchRequest(
la.config.UserBase,
lc.UserBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
la.config.UserFilter,
lc.UserFilter,
[]string{"dn", "uid", "gecos"}, nil))
if err != nil {
log.Warn("LDAP search error")
@ -198,7 +199,7 @@ func (la *LdapAuthenticator) Sync() error {
}
for username, where := range users {
if where == IN_DB && la.config.SyncDelOldUsers {
if where == IN_DB && lc.SyncDelOldUsers {
ur.DelUser(username)
log.Debugf("sync: remove %v (does not show up in LDAP anymore)", username)
} else if where == IN_LDAP {
@ -231,14 +232,15 @@ func (la *LdapAuthenticator) Sync() error {
// that so that connections can be reused/cached.
func (la *LdapAuthenticator) getLdapConnection(admin bool) (*ldap.Conn, error) {
conn, err := ldap.DialURL(la.config.Url)
lc := config.Keys.LdapConfig
conn, err := ldap.DialURL(lc.Url)
if err != nil {
log.Warn("LDAP URL dial failed")
return nil, err
}
if admin {
if err := conn.Bind(la.config.SearchDN, la.syncPassword); err != nil {
if err := conn.Bind(lc.SearchDN, la.syncPassword); err != nil {
conn.Close()
log.Warn("LDAP connection bind failed")
return nil, err

View File

@ -19,9 +19,7 @@ type LocalAuthenticator struct {
var _ Authenticator = (*LocalAuthenticator)(nil)
func (la *LocalAuthenticator) Init(
_ interface{}) error {
func (la *LocalAuthenticator) Init() error {
return nil
}

View File

@ -22,7 +22,6 @@ var Keys schema.ProgramConfig = schema.ProgramConfig{
Archive: json.RawMessage(`{\"kind\":\"file\",\"path\":\"./var/job-archive\"}`),
DisableArchive: false,
Validate: false,
LdapConfig: nil,
SessionMaxAge: "168h",
StopJobsExceedingWalltime: 0,
ShortRunningJobsDuration: 5 * 60,