Skip to content
Snippets Groups Projects
auth.go 6.12 KiB
Newer Older
  • Learn to ignore specific revisions
  • package server
    
    import (
    	"context"
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	"encoding/base64"
    
    	"errors"
    
    	"time"
    
    	apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
    
    	"code.fbi.h-da.de/danet/gosdn/controller/config"
    
    	rbacInterfaces "code.fbi.h-da.de/danet/gosdn/controller/interfaces/rbac"
    
    	"code.fbi.h-da.de/danet/gosdn/controller/metrics"
    
    	"code.fbi.h-da.de/danet/gosdn/controller/rbac"
    
    	"code.fbi.h-da.de/danet/gosdn/controller/store"
    
    	"github.com/bufbuild/protovalidate-go"
    
    	"github.com/prometheus/client_golang/prometheus"
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	"golang.org/x/crypto/argon2"
    
    	"google.golang.org/grpc/codes"
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	"google.golang.org/grpc/metadata"
    
    	"google.golang.org/grpc/status"
    
    	"google.golang.org/protobuf/reflect/protoreflect"
    
    // AuthServer holds a JWTManager and represents a AuthServiceServer.
    type AuthServer struct {
    
    	apb.UnimplementedAuthServiceServer
    
    	jwtManager     *rbac.JWTManager
    	userService    rbacInterfaces.UserService
    	protoValidator *protovalidate.Validator
    
    // NewAuthServer receives a JWTManager and a userService and returns a new Auth interface.
    
    func NewAuthServer(
    	jwtManager *rbac.JWTManager,
    	userService rbacInterfaces.UserService,
    	protoValidator *protovalidate.Validator,
    ) *AuthServer {
    
    	return &AuthServer{
    
    		jwtManager:     jwtManager,
    		userService:    userService,
    		protoValidator: protoValidator,
    
    func (s AuthServer) checkForValidationErrors(request protoreflect.ProtoMessage) error {
    	err := s.protoValidator.Validate(request)
    	if err != nil {
    		var valErr *protovalidate.ValidationError
    
    		if ok := errors.As(err, &valErr); ok {
    			protoErr := valErr.ToProto()
    			grpcError, _ := status.New(codes.Aborted, "Validation failed").WithDetails(protoErr)
    
    			return grpcError.Err()
    		}
    
    		return status.Errorf(codes.Aborted, "%v", err)
    	}
    
    	return nil
    }
    
    
    func (s AuthServer) Login(ctx context.Context, request *apb.LoginRequest) (*apb.LoginResponse, error) {
    
    	labels := prometheus.Labels{"service": "auth", "rpc": "post"}
    
    	start := metrics.StartHook(labels, grpcRequestsTotal)
    	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
    
    
    	err := s.checkForValidationErrors(request)
    	if err != nil {
    		return nil, err
    
    		UserName: request.Username,
    
    		Password: request.Pwd,
    	}
    
    	// validation of credentials
    
    	err = s.isValidUser(user)
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	if err != nil {
    
    		return nil, err
    	}
    
    	// generate token, persist session and return to user
    	token, err := s.jwtManager.GenerateToken(user)
    	if err != nil {
    		return nil, err
    	}
    
    
    	userToUpdate, err := s.userService.Get(store.Query{Name: user.UserName})
    
    	if err != nil {
    		return nil, err
    	}
    
    
    Neil-Jocelyn Schark's avatar
    Neil-Jocelyn Schark committed
    	err = addTokenAndEnsureTokenLimit(userToUpdate, token)
    	if err != nil {
    		return nil, err
    
    	err = s.userService.Update(userToUpdate)
    
    	if err != nil {
    		return nil, err
    	}
    
    
    	return &apb.LoginResponse{
    		Timestamp: time.Now().UnixNano(),
    
    func (s AuthServer) Logout(ctx context.Context, request *apb.LogoutRequest) (*apb.LogoutResponse, error) {
    
    	labels := prometheus.Labels{"service": "auth", "rpc": "post"}
    
    	start := metrics.StartHook(labels, grpcRequestsTotal)
    	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
    
    
    	err := s.checkForValidationErrors(request)
    	if err != nil {
    		return nil, err
    
    	err = s.handleLogout(ctx, request.Username)
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	if err != nil {
    		return nil, err
    	}
    
    
    	return &apb.LogoutResponse{
    		Timestamp: time.Now().UnixNano(),
    	}, nil
    }
    
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    // isValidUser checks if the provided user name fits to a stored one and then checks if the provided password is correct.
    
    func (s AuthServer) isValidUser(user rbac.User) error {
    
    	storedUser, err := s.userService.Get(store.Query{Name: user.Name()})
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    		return err
    
    	if storedUser.Name() == user.Name() {
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    		err := s.isCorrectPassword(storedUser.GetPassword(), storedUser.GetSalt(), user.Password)
    		if err != nil {
    			return err
    		}
    	}
    
    	return nil
    }
    
    // isCorrectPassword checks if the provided password fits with the hashed user password taken from the storage.
    
    func (s AuthServer) isCorrectPassword(storedPassword, salt, loginPassword string) error {
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	hashedPasswordFromLogin := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(loginPassword), []byte(salt), 1, 64*1024, 4, 32))
    
    	if storedPassword == hashedPasswordFromLogin {
    		return nil
    	}
    
    	return status.Errorf(codes.Unauthenticated, "incorrect user name or password")
    }
    
    // handleLogout checks if the provided user name matches with the one associated with token and
    
    Neil-Jocelyn Schark's avatar
    Neil-Jocelyn Schark committed
    // removed the token from all tokens of the user.
    
    func (s AuthServer) handleLogout(ctx context.Context, userName string) error {
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	md, ok := metadata.FromIncomingContext(ctx)
    	if !ok {
    		return status.Errorf(codes.Aborted, "metadata is not provided")
    	}
    
    	if len(md["authorize"]) > 0 {
    		token := md["authorize"][0]
    
    		claims, err := s.jwtManager.GetClaimsFromToken(token)
    		if err != nil {
    			return err
    		}
    
    		if claims.Username != userName {
    			return status.Errorf(codes.Aborted, "missing match of user associated to token and provided user name")
    		}
    
    
    		storedUser, err := s.userService.Get(store.Query{Name: userName})
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    		if err != nil {
    			return err
    		}
    
    
    		storedTokens := storedUser.GetTokens()
    		foundToken := false
    		for _, storedToken := range storedTokens {
    			if storedToken == token {
    
    Neil-Jocelyn Schark's avatar
    Neil-Jocelyn Schark committed
    				err := storedUser.RemoveToken(token)
    				if err != nil {
    					return status.Errorf(codes.Aborted, "error checking match of token provied for user")
    				}
    
    				foundToken = true
    				break
    			}
    		}
    
    		if !foundToken {
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    			return status.Errorf(codes.Aborted, "missing match of token provied for user")
    		}
    
    
    		err = s.userService.Update(&rbac.User{UserID: storedUser.ID(),
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    			UserName: storedUser.Name(),
    			Roles:    storedUser.GetRoles(),
    			Password: storedUser.GetPassword(),
    
    			Tokens:   storedUser.GetTokens(),
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    			Salt:     storedUser.GetSalt(),
    
    			Metadata: storedUser.GetMetadata(),
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    		})
    
    		if err != nil {
    			return err
    
    Fabian Seidl's avatar
    Fabian Seidl committed
    	return nil
    
    Neil-Jocelyn Schark's avatar
    Neil-Jocelyn Schark committed
    
    func addTokenAndEnsureTokenLimit(userToUpdate rbacInterfaces.User, token string) error {
    	userToUpdate.AddToken(token)
    	for len(userToUpdate.GetTokens()) > config.MaxTokensPerUser {
    		err := userToUpdate.RemoveToken(userToUpdate.GetTokens()[0])
    		if err != nil {
    			return err
    		}
    	}
    	return nil
    }