package authtokens import ( "golang.org/x/crypto/bcrypt" "src.dualinventive.com/go/authentication-service/internal/domain" "src.dualinventive.com/go/authentication-service/internal/pwreset" "src.dualinventive.com/go/authentication-service/internal/storage" "src.dualinventive.com/go/lib/dilog" ) //Service contains token related operations. type Service struct { Logger dilog.Logger CredentialsRepository storage.CredentialsRepository TokenRepository storage.TokenRepository ResetCodeRepository storage.ResetCodeRepository TemplateRepository pwreset.TemplateRepository EmailSender pwreset.EmailSender } //NewService creates an authtokens.Service func NewService( logger dilog.Logger, credentialsrepository storage.CredentialsRepository, tokenrepository storage.TokenRepository, resetcoderepository storage.ResetCodeRepository, templaterepository pwreset.TemplateRepository, emailsender pwreset.EmailSender, ) *Service { service := &Service{ Logger: logger, CredentialsRepository: credentialsrepository, TokenRepository: tokenrepository, ResetCodeRepository: resetcoderepository, TemplateRepository: templaterepository, EmailSender: emailsender, } return service } //CreateToken is used to create a new token for user func (s *Service) CreateToken( credentials domain.Credentials, userAgent string, ) (token *domain.Token, err error) { user, err := s.CredentialsRepository.GetUserByUserName(credentials.User, credentials.CompanyCode) if err != nil { s.Logger.WithError(err).WithField("user", credentials.User).WithField("company", credentials.CompanyCode). Error("CredentialsRepository.GetUserByUserName returned error") return nil, &ErrInvalidCredentials{} } if !validatePassword(credentials.Password, user.PasswordHash) { return nil, &ErrInvalidCredentials{} } token, err = s.TokenRepository. CreateToken(credentials.User, credentials.CompanyCode, userAgent, resolveRights(user)...) if err != nil { s.Logger.WithError(err).WithField("user", credentials.User). WithField("company", credentials.CompanyCode).WithField("agent", userAgent). Error("TokenRepository.CreateToken returned error") return nil, NewErrRepositoryConnectionFailure("TokenRepository") } s.Logger.WithField("user", credentials.User).WithField("company", credentials.CompanyCode). WithField("agent", userAgent).WithField("token", token.Secret[:20]+"..."). Debug("Service.CreateToken returned successful") return token, nil } //resolveRights deduplicates rights of a user by turning them into a set-like map. //This set is then returned as an array. func resolveRights(user *domain.User) []string { set := make(map[string]bool) for _, role := range user.Roles { for _, right := range role.Rights { set[right.Code] = true } } rights := make([]string, len(set)) for right := range set { rights = append(rights, right) } return rights } //VerifyToken is used to verify if token is valid func (s *Service) VerifyToken( token *domain.Token, rights ...string, ) (*domain.Token, error) { if token == nil { s.Logger.Warning("Service.VerifyToken got nil token") return nil, &ErrNilToken{} } isValid, isAuthorized, err := s.TokenRepository.IsValid(token, rights...) if !isValid { switch err { case storage.ErrTokenNotFound: s.Logger.WithField("token", token). Error("TokenRepository.IsValid returned ErrTokenNotFound") return nil, &ErrInvalidToken{} case storage.ErrTokenInvalid: s.Logger.WithField("token", token). Error("TokenRepository.IsValid returned ErrTokenInvalid") return nil, &ErrInvalidToken{} default: s.Logger.WithField("token", token). Error("TokenRepository.IsValid returned false") return nil, &ErrInvalidToken{} } } if isValid && !isAuthorized { return nil, &ErrUnauthorized{} } s.Logger.WithField("token", token.Secret[:20]+"..."). Debug("Service.VerifyToken returned successful") return token, nil } //DeleteToken is used to remove the token for user func (s *Service) DeleteToken( token *domain.Token, ) error { if token == nil { s.Logger.Warning("Service.DeleteToken got nil token") return &ErrNilToken{} } err := s.TokenRepository.DeleteToken(token) if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.DeleteToken returned error") return &ErrTokenNotFound{} } s.Logger.WithField("token", token.Secret[:20]+"..."). Debug("Service.DeleteToken returned successful") return nil } //GetUserByToken is used to retrieve user information based on the given token. func (s *Service) GetUserByToken( token *domain.Token, ) (*domain.User, error) { if token == nil { s.Logger.Warning("Service.GetUserByToken got nil token") return nil, &ErrNilToken{} } userName, companyCode, err := s.TokenRepository.GetUserNameByToken(token) if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.GetUserNameByToken returned error") return nil, &ErrTokenNotFound{} } user, err := s.CredentialsRepository.GetUserByUserName(userName, companyCode) if err != nil { s.Logger.WithError(err).WithField("user", userName).WithField("company", companyCode). Error("CredentialsRepository.GetUserByUserName returned error") return nil, NewErrUserNotFound(userName) } if user == nil { s.Logger.WithField("user", userName).WithField("company", companyCode). Warning("CredentialsRepository.GetUserByUserName returned nil") return nil, NewErrUserNotFound(userName) } s.Logger.WithField("token", token.Secret[:20]+"...").WithField("user", user). Debug("Service.GetUserByToken returned successful") return user, nil } //GetTokensByUser returns a list of currently active tokens by the given username. func (s *Service) GetTokensByUser(userName string, companyCode string) ([]*domain.Token, error) { if userName == "" { s.Logger.WithField("user", userName).WithField("company", companyCode). Warning("Service.GetTokensByUser got empty userName") return nil, NewErrUserNotFound(userName) } if companyCode == "" { s.Logger.WithField("user", userName).WithField("company", companyCode). Warning("Service.GetTokensByUser got empty companyCode") return nil, NewErrUserNotFound(userName) } tokens, err := s.TokenRepository.GetTokens(userName, companyCode) if err != nil { s.Logger.WithError(err).WithField("user", userName).WithField("company", companyCode). Error("TokenRepository.GetTokens returned error") return nil, NewErrFailedToGetTokens(userName, err) } s.Logger.WithField("user", userName).WithField("company", companyCode).WithField("token", tokens). Debug("Service.GetTokensByUser returned successful") return tokens, nil } //GetOpaqueTokensByToken returns a list of active session for the current user. func (s *Service) GetOpaqueTokensByToken(token *domain.Token) (map[string]string, error) { opaqueTokens := make(map[string]string) if token == nil { s.Logger.Warning("Service.GetOpaqueTokensByToken got nil token") return nil, &ErrNilToken{} } isValid, _, err := s.TokenRepository.IsValid(token) if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.IsValid returned error") return nil, &ErrInvalidToken{} } if !isValid { s.Logger.WithField("token", token). Warning("TokenRepository.IsValid returned false") return nil, &ErrInvalidToken{} } userName, companyCode, err := s.TokenRepository.GetUserNameByToken(token) if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.GetUserNameByToken returned error") return nil, NewErrRepositoryConnectionFailure("TokenRepository") } tokens, err := s.GetTokensByUser(userName, companyCode) if err != nil { s.Logger.WithError(err).WithField("user", userName).WithField("company", companyCode). Error("GetTokensByUser returned error") // err is NewErrUserNotFound or NewErrFailedToGetTokens return nil, err } for _, token := range tokens { opaqueID, userAgent, err := s.TokenRepository.GetTokenPayload(token) if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.GetTokenPayload returned error") } else { opaqueTokens[opaqueID] = userAgent } } s.Logger.WithField("token", token.Secret[:20]+"...").WithField("opaque", opaqueTokens). Debug("Service.GetOpaqueTokensByToken returned successful") return opaqueTokens, nil } //DeleteTokenByOpaqueID removes a token by reference of it's opaque id. func (s *Service) DeleteTokenByOpaqueID(token *domain.Token, opaqueTokenID string) error { if token == nil { s.Logger.Warning("Service.DeleteTokenByOpaqueID got nil token") return &ErrNilToken{} } isValid, _, err := s.TokenRepository.IsValid(token) if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.IsValid returned error") return &ErrInvalidToken{} } if !isValid { s.Logger.WithField("token", token). Warning("TokenRepository.IsValid returned false") return &ErrInvalidToken{} } tokenMap := make(map[string]*domain.Token) userName, companyCode, err := s.TokenRepository.GetUserNameByToken(token) if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.GetUserNameByToken returned error") return NewErrRepositoryConnectionFailure("TokenRepository") } tokens, err := s.GetTokensByUser(userName, companyCode) if err != nil { s.Logger.WithError(err).WithField("user", userName).WithField("company", companyCode). Error("TokenRepository.GetTokensByUser returned error") // err is NewErrUserNotFound or NewErrFailedToGetTokens return err } for _, token := range tokens { opaqueID, _, err := s.TokenRepository.GetTokenPayload(token) //nolint: vetshadow if err != nil { s.Logger.WithError(err).WithField("token", token). Error("TokenRepository.GetTokenPayload returned error") } else { tokenMap[opaqueID] = token } } token, ok := tokenMap[opaqueTokenID] if !ok { s.Logger.Warning("Service.DeleteTokenByOpaqueID token not found") return &ErrTokenNotFound{} } err = s.DeleteToken(token) if err != nil { // err is ErrNilToken or ErrTokenNotFound return err } s.Logger.WithField("token", token.Secret[:20]+"...").WithField("opaque", opaqueTokenID). Debug("Service.DeleteTokenByOpaqueID returned successful") return nil } func validatePassword(password string, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil }