mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 05:19:05 +01:00
Add login via JWT
This commit is contained in:
parent
5041685df1
commit
23f6015494
@ -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)}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
rawtoken = rawtoken[len(prefix):]
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user