mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-11-10 08:57:25 +01:00
Add login via JWT
This commit is contained in:
parent
5041685df1
commit
23f6015494
@ -1,8 +1,10 @@
|
||||
package authv2
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
@ -29,6 +31,7 @@ type User struct {
|
||||
Roles []string `json:"roles"`
|
||||
AuthSource int8 `json:"via"`
|
||||
Email string `json:"email"`
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
func (u *User) HasRole(role string) bool {
|
||||
@ -43,7 +46,7 @@ func (u *User) HasRole(role string) bool {
|
||||
type Authenticator interface {
|
||||
Init(auth *Authentication, config json.RawMessage) error
|
||||
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)
|
||||
}
|
||||
|
||||
@ -75,7 +78,28 @@ func Init(db *sqlx.DB) (*Authentication, error) {
|
||||
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)
|
||||
cols := []string{"username", "password", "roles"}
|
||||
vals := []interface{}{user.Username, user.Password, string(rolesJson)}
|
||||
|
@ -2,12 +2,14 @@ package authv2
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
@ -17,6 +19,8 @@ type JWTAuthenticator struct {
|
||||
auth *Authentication
|
||||
publicKey ed25519.PublicKey
|
||||
privateKey ed25519.PrivateKey
|
||||
|
||||
maxAge time.Duration
|
||||
}
|
||||
|
||||
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") != ""
|
||||
}
|
||||
|
||||
func (ja *JWTAuthenticator) Login(user *User, password string, rw http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
func (ja *JWTAuthenticator) Login(_ *User, password string, rw http.ResponseWriter, r *http.Request) (*User, error) {
|
||||
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) {
|
||||
rawtoken := r.Header.Get("X-Auth-Token")
|
||||
if rawtoken == "" {
|
||||
rawtoken = r.Header.Get("Authorization")
|
||||
prefix := "Bearer "
|
||||
if !strings.HasPrefix(rawtoken, prefix) {
|
||||
return nil, nil
|
||||
rawtoken = strings.TrimPrefix("Bearer ", rawtoken)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@ -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{
|
||||
Username: sub,
|
||||
Roles: roles,
|
||||
AuthSource: AuthViaToken,
|
||||
}, 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)
|
||||
}
|
||||
|
@ -70,19 +70,19 @@ func (la *LdapAutnenticator) CanLogin(user *User, rw http.ResponseWriter, r *htt
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
userDn := strings.Replace(la.config.UserBind, "{username}", user.Username, -1)
|
||||
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) {
|
||||
|
@ -23,12 +23,12 @@ func (la *LocalAuthenticator) CanLogin(user *User, rw http.ResponseWriter, r *ht
|
||||
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 {
|
||||
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) {
|
||||
|
Loading…
Reference in New Issue
Block a user