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-14 13:52:29 +02:00
commit fe6de5bc68
9 changed files with 91 additions and 58 deletions

View File

@ -1,11 +1,16 @@
# `cc-backend` version 1.1.0
# `cc-backend` version 1.2.0
Supports job archive version 1 and database version 6.
This is a minor release of `cc-backend`, the API backend and frontend
implementation of ClusterCockpit.
** Breaking changes v1 **
** Breaking changes **
The LDAP configuration option user_filter was changed and now should not include
the wildcard. Example:
* Old: `"user_filter": "(&(objectclass=posixAccount)(uid=*))"`
* New: `"user_filter": "&(objectclass=posixAccount)"`
The aggregate job statistic core hours is now computed using the job table
column `num_hwthreads`. In a future release this column will be renamed to

View File

@ -124,42 +124,8 @@ It is first checked if the required configuration keys are set:
```
The Login function:
# Auth
The Auth function (located in `auth.go`):
* Returns a new http handler function that is defined right away
* This handler iterates over all authenticators
* Calls `Auth()` on every authenticator
* If err is not nil and the user object is valid it puts the user object in the
request context and starts the onSuccess http handler
* Otherwise it calls the onFailure handler
## Local
Calls the `AuthViaSession()` function in `auth.go`. This will extract username,
projects and roles from the session and initialize a user object with those
values.
## LDAP
Calls the `AuthViaSession()` function in `auth.go`. This will extract username,
projects and roles from the session and initialize a user object with those
values.
# JWT
Check for JWT token:
* Is token passed in the `X-Auth-Token` or `Authorization` header
* If no token is found in a header it tries to read the token from a configured
cookie.
Finally it calls AuthViaSession in `auth.go` if a valid session exists. This is
true if a JWT token was previously used to initiate a session. In this case the
user object initialized with the session is returned right away.
In case a token was found extract and parse the token:
* Check if signing method is Ed25519/EdDSA
* Extracts and parses the token
* Checks if signing method is Ed25519/EdDSA
* In case publicKeyCrossLogin is configured:
- Check if `iss` issuer claim matched trusted issuer from configuration
- Return public cross login key
@ -167,7 +133,34 @@ In case a token was found extract and parse the token:
* Check if claims are valid
* Depending on the option `ForceJWTValidationViaDatabase ` the roles are
extracted from JWT token or taken from user object fetched from database
* In case the token was extracted from cookie create a new session and ask the
browser to delete the JWT cookie
* Ask browser to delete the JWT cookie
* Return valid user object
# Auth
The Auth function (located in `auth.go`):
* Returns a new http handler function that is defined right away
* This handler tries two methods to authenticate a user:
- Via a JWT API token in `AuthViaJWT()`
- Via a valid session in `AuthViaSession()`
* If err is not nil and the user object is valid it puts the user object in the
request context and starts the onSuccess http handler
* Otherwise it calls the onFailure handler
## AuthViaJWT
Implemented in JWTAuthenticator:
* Extract token either from header `X-Auth-Token` or `Authorization` with Bearer
prefix
* Parse token and check if it is valid. The Parse routine will also check if the
token is expired.
* If the option `ForceJWTValidationViaDatabase` is set it will ensure the
user object exists in the database and takes the roles from the database user
* Otherwise the roles are extracted from the roles claim
* Returns a valid user object with AuthType set to AuthToken
## AuthViaSession
* Extracts session
* Get values username, projects, and roles from session
* Returns a valid user object with AuthType set to AuthSession

View File

@ -43,7 +43,6 @@ type User struct {
AuthSource AuthSource `json:"authSource"`
Email string `json:"email"`
Projects []string `json:"projects"`
Expiration time.Time
}
func (u *User) HasProject(project string) bool {
@ -66,7 +65,7 @@ func GetUser(ctx context.Context) *User {
type Authenticator interface {
Init(auth *Authentication, config interface{}) error
CanLogin(user *User, rw http.ResponseWriter, r *http.Request) bool
CanLogin(user *User, username string, rw http.ResponseWriter, r *http.Request) bool
Login(user *User, rw http.ResponseWriter, r *http.Request) (*User, error)
}
@ -208,7 +207,7 @@ func (auth *Authentication) Login(
}
for _, authenticator := range auth.authenticators {
if !authenticator.CanLogin(dbUser, rw, r) {
if !authenticator.CanLogin(dbUser, username, rw, r) {
continue
}

View File

@ -86,11 +86,6 @@ func (ja *JWTAuthenticator) AuthViaJWT(
// Token is valid, extract payload
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
exp, _ := claims["exp"].(float64)
if exp < float64(time.Now().Unix()) {
return nil, errors.New("token is expired")
}
var roles []string

View File

@ -10,7 +10,6 @@ import (
"errors"
"net/http"
"os"
"time"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -91,6 +90,7 @@ func (ja *JWTCookieSessionAuthenticator) Init(auth *Authentication, conf interfa
func (ja *JWTCookieSessionAuthenticator) CanLogin(
user *User,
username string,
rw http.ResponseWriter,
r *http.Request) bool {
@ -140,7 +140,7 @@ func (ja *JWTCookieSessionAuthenticator) Login(
return ja.publicKey, nil
})
if err != nil {
log.Warn("Error while parsing token")
log.Warn("error while parsing token")
return nil, err
}
@ -152,7 +152,6 @@ func (ja *JWTCookieSessionAuthenticator) Login(
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
exp, _ := claims["exp"].(float64)
var name string
if val, ok := claims["name"]; ok {
@ -201,6 +200,5 @@ func (ja *JWTCookieSessionAuthenticator) Login(
}
}
user.Expiration = time.Unix(int64(exp), 0)
return user, nil
}

View File

@ -10,7 +10,6 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/golang-jwt/jwt/v4"
@ -42,6 +41,7 @@ func (ja *JWTSessionAuthenticator) Init(auth *Authentication, conf interface{})
func (ja *JWTSessionAuthenticator) CanLogin(
user *User,
username string,
rw http.ResponseWriter,
r *http.Request) bool {
@ -76,7 +76,6 @@ func (ja *JWTSessionAuthenticator) Login(
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
exp, _ := claims["exp"].(float64)
var name string
// Java/Grails Issued Token
@ -131,6 +130,5 @@ func (ja *JWTSessionAuthenticator) Login(
}
}
user.Expiration = time.Unix(int64(exp), 0)
return user, nil
}

View File

@ -66,10 +66,51 @@ func (la *LdapAuthenticator) Init(
func (la *LdapAuthenticator) CanLogin(
user *User,
username string,
rw http.ResponseWriter,
r *http.Request) bool {
return user != nil && user.AuthSource == AuthViaLDAP
if user != nil && user.AuthSource == AuthViaLDAP {
return true
} else {
if la.config.SyncUserOnLogin {
l, err := la.getLdapConnection(true)
if err != nil {
log.Error("LDAP connection error")
}
// Search for the given username
searchRequest := ldap.NewSearchRequest(
la.config.UserBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(%s(uid=%s))", la.config.UserFilter, username),
[]string{"dn", "uid", "gecos"}, nil)
sr, err := l.Search(searchRequest)
if err != nil {
log.Warn(err)
return false
}
if len(sr.Entries) != 1 {
log.Warn("User does not exist or too many entries returned")
return false
}
entry := sr.Entries[0]
name := entry.GetAttributeValue("gecos")
if _, err := la.auth.db.Exec(`INSERT INTO user (username, ldap, name, roles) VALUES (?, ?, ?, ?)`,
username, 1, name, "[\""+GetRoleString(RoleUser)+"\"]"); err != nil {
log.Errorf("User '%s' new in LDAP: Insert into DB failed", username)
return false
}
return true
}
}
return false
}
func (la *LdapAuthenticator) Login(
@ -124,8 +165,10 @@ func (la *LdapAuthenticator) Sync() error {
defer l.Close()
ldapResults, err := l.Search(ldap.NewSearchRequest(
la.config.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
la.config.UserFilter, []string{"dn", "uid", "gecos"}, nil))
la.config.UserBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(%s(uid=%s))", la.config.UserFilter, "*"),
[]string{"dn", "uid", "gecos"}, nil))
if err != nil {
log.Warn("LDAP search error")
return err

View File

@ -28,6 +28,7 @@ func (la *LocalAuthenticator) Init(
func (la *LocalAuthenticator) CanLogin(
user *User,
username string,
rw http.ResponseWriter,
r *http.Request) bool {

View File

@ -17,6 +17,7 @@ type LdapConfig struct {
UserFilter string `json:"user_filter"`
SyncInterval string `json:"sync_interval"` // Parsed using time.ParseDuration.
SyncDelOldUsers bool `json:"sync_del_old_users"`
SyncUserOnLogin bool `json:"syncUserOnLogin"`
}
type JWTAuthConfig struct {