Add login via JWT

This commit is contained in:
Lou Knauer 2022-07-07 12:48:04 +02:00
parent 5041685df1
commit 23f6015494
4 changed files with 119 additions and 16 deletions

View File

@ -1,8 +1,10 @@
package authv2 package authv2
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"net/http" "net/http"
"time"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
@ -29,6 +31,7 @@ type User struct {
Roles []string `json:"roles"` Roles []string `json:"roles"`
AuthSource int8 `json:"via"` AuthSource int8 `json:"via"`
Email string `json:"email"` Email string `json:"email"`
Expiration time.Time
} }
func (u *User) HasRole(role string) bool { func (u *User) HasRole(role string) bool {
@ -43,7 +46,7 @@ func (u *User) HasRole(role string) bool {
type Authenticator interface { type Authenticator interface {
Init(auth *Authentication, config json.RawMessage) error Init(auth *Authentication, config json.RawMessage) error
CanLogin(user *User, rw http.ResponseWriter, r *http.Request) bool CanLogin(user *User, rw http.ResponseWriter, r *http.Request) bool
Login(user *User, password string, rw http.ResponseWriter, r *http.Request) error Login(user *User, password string, rw http.ResponseWriter, r *http.Request) (*User, error)
Auth(rw http.ResponseWriter, r *http.Request) (*User, error) Auth(rw http.ResponseWriter, r *http.Request) (*User, error)
} }
@ -75,7 +78,28 @@ func Init(db *sqlx.DB) (*Authentication, error) {
return auth, nil return auth, nil
} }
func (auth *Authentication) RegisterUser(user *User) error { func (auth *Authentication) GetUser(username string) (*User, error) {
user := &User{Username: username}
var hashedPassword, name, rawRoles, email sql.NullString
if err := sq.Select("password", "ldap", "name", "roles", "email").From("user").
Where("user.username = ?", username).RunWith(auth.db).
QueryRow().Scan(&hashedPassword, &user.AuthSource, &name, &rawRoles, &email); err != nil {
return nil, err
}
user.Password = hashedPassword.String
user.Name = name.String
user.Email = email.String
if rawRoles.Valid {
if err := json.Unmarshal([]byte(rawRoles.String), &user.Roles); err != nil {
return nil, err
}
}
return user, nil
}
func (auth *Authentication) AddUser(user *User) error {
rolesJson, _ := json.Marshal(user.Roles) rolesJson, _ := json.Marshal(user.Roles)
cols := []string{"username", "password", "roles"} cols := []string{"username", "password", "roles"}
vals := []interface{}{user.Username, user.Password, string(rolesJson)} vals := []interface{}{user.Username, user.Password, string(rolesJson)}

View File

@ -2,12 +2,14 @@ package authv2
import ( import (
"crypto/ed25519" "crypto/ed25519"
"database/sql"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
@ -17,6 +19,8 @@ type JWTAuthenticator struct {
auth *Authentication auth *Authentication
publicKey ed25519.PublicKey publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
maxAge time.Duration
} }
var _ Authenticator = (*JWTAuthenticator)(nil) var _ Authenticator = (*JWTAuthenticator)(nil)
@ -47,19 +51,76 @@ func (ja *JWTAuthenticator) CanLogin(user *User, rw http.ResponseWriter, r *http
return user.AuthSource == AuthViaToken || r.Header.Get("Authorization") != "" return user.AuthSource == AuthViaToken || r.Header.Get("Authorization") != ""
} }
func (ja *JWTAuthenticator) Login(user *User, password string, rw http.ResponseWriter, r *http.Request) error { func (ja *JWTAuthenticator) Login(_ *User, password string, rw http.ResponseWriter, r *http.Request) (*User, error) {
return nil rawtoken := r.Header.Get("X-Auth-Token")
if rawtoken == "" {
rawtoken = r.Header.Get("Authorization")
rawtoken = strings.TrimPrefix("Bearer ", rawtoken)
}
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
if t.Method != jwt.SigningMethodEdDSA {
return nil, errors.New("only Ed25519/EdDSA supported")
}
return ja.publicKey, nil
})
if err != nil {
return nil, err
}
if err := token.Claims.Valid(); err != nil {
return nil, err
}
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
exp, _ := claims["exp"].(float64)
var roles []string
if rawroles, ok := claims["roles"].([]interface{}); ok {
for _, rr := range rawroles {
if r, ok := rr.(string); ok {
roles = append(roles, r)
}
}
}
user, err := ja.auth.GetUser(sub)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
if err != nil && err == sql.ErrNoRows {
user = &User{
Username: user.Username,
Roles: roles,
AuthSource: AuthViaToken,
}
if err := ja.auth.AddUser(user); err != nil {
return nil, err
}
}
user.Expiration = time.Unix(int64(exp), 0)
return user, nil
} }
func (ja *JWTAuthenticator) Auth(rw http.ResponseWriter, r *http.Request) (*User, error) { func (ja *JWTAuthenticator) Auth(rw http.ResponseWriter, r *http.Request) (*User, error) {
rawtoken := r.Header.Get("X-Auth-Token") rawtoken := r.Header.Get("X-Auth-Token")
if rawtoken == "" { if rawtoken == "" {
rawtoken = r.Header.Get("Authorization") rawtoken = r.Header.Get("Authorization")
prefix := "Bearer " rawtoken = strings.TrimPrefix("Bearer ", rawtoken)
if !strings.HasPrefix(rawtoken, prefix) { }
return nil, nil
// Because a user can also log in via a token, the
// session cookie must be checked here as well:
if rawtoken == "" {
user, err := ja.auth.AuthViaSession(rw, r)
if err != nil {
return nil, err
} }
rawtoken = rawtoken[len(prefix):]
user.AuthSource = AuthViaToken
return user, nil
} }
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) { token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
@ -88,10 +149,28 @@ func (ja *JWTAuthenticator) Auth(rw http.ResponseWriter, r *http.Request) (*User
} }
} }
// TODO: Check if sub is still a valid user!
return &User{ return &User{
Username: sub, Username: sub,
Roles: roles, Roles: roles,
AuthSource: AuthViaToken, AuthSource: AuthViaToken,
}, nil }, nil
} }
// Generate a new JWT that can be used for authentication
func (ja *JWTAuthenticator) ProvideJWT(user *User) (string, error) {
if ja.privateKey == nil {
return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set")
}
now := time.Now()
claims := jwt.MapClaims{
"sub": user.Username,
"roles": user.Roles,
"iat": now.Unix(),
}
if ja.maxAge != 0 {
claims["exp"] = now.Add(ja.maxAge).Unix()
}
return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(ja.privateKey)
}

View File

@ -70,19 +70,19 @@ func (la *LdapAutnenticator) CanLogin(user *User, rw http.ResponseWriter, r *htt
return user.AuthSource == AuthViaLDAP return user.AuthSource == AuthViaLDAP
} }
func (la *LdapAutnenticator) Login(user *User, password string, rw http.ResponseWriter, r *http.Request) error { func (la *LdapAutnenticator) Login(user *User, password string, rw http.ResponseWriter, r *http.Request) (*User, error) {
l, err := la.getLdapConnection(false) l, err := la.getLdapConnection(false)
if err != nil { if err != nil {
return err return nil, err
} }
defer l.Close() defer l.Close()
userDn := strings.Replace(la.config.UserBind, "{username}", user.Username, -1) userDn := strings.Replace(la.config.UserBind, "{username}", user.Username, -1)
if err := l.Bind(userDn, password); err != nil { if err := l.Bind(userDn, password); err != nil {
return err return nil, err
} }
return nil return user, nil
} }
func (la *LdapAutnenticator) Auth(rw http.ResponseWriter, r *http.Request) (*User, error) { func (la *LdapAutnenticator) Auth(rw http.ResponseWriter, r *http.Request) (*User, error) {

View File

@ -23,12 +23,12 @@ func (la *LocalAuthenticator) CanLogin(user *User, rw http.ResponseWriter, r *ht
return user.AuthSource == AuthViaLocalPassword return user.AuthSource == AuthViaLocalPassword
} }
func (la *LocalAuthenticator) Login(user *User, password string, rw http.ResponseWriter, r *http.Request) error { func (la *LocalAuthenticator) Login(user *User, password string, rw http.ResponseWriter, r *http.Request) (*User, error) {
if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); e != nil { if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); e != nil {
return fmt.Errorf("user '%s' provided the wrong password (%s)", user.Username, e.Error()) return nil, fmt.Errorf("user '%s' provided the wrong password (%w)", user.Username, e)
} }
return nil return user, nil
} }
func (la *LocalAuthenticator) Auth(rw http.ResponseWriter, r *http.Request) (*User, error) { func (la *LocalAuthenticator) Auth(rw http.ResponseWriter, r *http.Request) (*User, error) {