eintopf.info@v0.13.16/service/auth/auth.go (about)

     1  // Copyright (C) 2022 The Eintopf authors
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  
    16  package auth
    17  
    18  import (
    19  	"context"
    20  	"crypto/rsa"
    21  	"fmt"
    22  	"time"
    23  
    24  	"github.com/golang-jwt/jwt/v4"
    25  
    26  	"eintopf.info/internal/xerror"
    27  )
    28  
    29  // Service defines an authentication service.
    30  // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/auth Service --output=../../internal/mock/auth_service.go --package=mock --mock-name=AuthService
    31  type Service interface {
    32  	// Login takes an email and a password. When valid returns a new JWT.
    33  	Login(ctx context.Context, email, password string) (token string, err error)
    34  
    35  	// Authenticate takes a JWT and checks wether it's valid.
    36  	// If it is valid. It returns the stored id.
    37  	Authenticate(token string) (id string, err error)
    38  
    39  	// Authorize authorizes the given user identifier by returning its
    40  	// authorization role.
    41  	Authorize(ctx context.Context, id string) (role Role, err error)
    42  }
    43  
    44  // Authenticator determines wether a set of credentials are valid.
    45  type Authenticator interface {
    46  	// Validate takes an email and password and checks if they are valid.
    47  	// If it is valid it returns a unique identifier corresponding to that
    48  	// identifier.
    49  	// It might return an error, when something unexpected happens during
    50  	// validation.
    51  	Validate(ctx context.Context, email, password string) (id string, err error)
    52  }
    53  
    54  // Role defines an authorization role.
    55  type Role string
    56  
    57  // Authorization roles.
    58  const (
    59  	RoleInternal  Role = "internal"
    60  	RoleAdmin     Role = "admin"
    61  	RoleModerator Role = "moderator"
    62  	RoleNormal    Role = "normal"
    63  )
    64  
    65  // Authorizer checks the authorization for a given identifier.
    66  type Authorizer interface {
    67  	// Role returns the role for the given identifier.
    68  	Role(ctx context.Context, id string) (role Role, err error)
    69  }
    70  
    71  // NewService returns a new authentication service.
    72  func NewService(authenticator Authenticator, authorizer Authorizer, privateKey, publicKey []byte, tz *time.Location) (Service, error) {
    73  	signKey, verifyKey, err := ParseAuthKeys(privateKey, publicKey)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	return &service{authenticator, authorizer, verifyKey, signKey, tz}, nil
    79  }
    80  
    81  type service struct {
    82  	authenticator Authenticator
    83  	authorizer    Authorizer
    84  
    85  	verifyKey *rsa.PublicKey
    86  	signKey   *rsa.PrivateKey
    87  	tz        *time.Location
    88  }
    89  
    90  // ErrInvalidCredentials is the error, that gets returned by the login method, when
    91  // the provided credentials where invalid.
    92  var ErrInvalidCredentials = fmt.Errorf("invalid credentials")
    93  
    94  // ErrDeactivated indicates, that the user is deactivated.
    95  var ErrDeactivated = fmt.Errorf("deactivated")
    96  
    97  func (s *service) Login(ctx context.Context, email, password string) (string, error) {
    98  	id, err := s.authenticator.Validate(ctx, email, password)
    99  	if err != nil {
   100  		return "", err
   101  	}
   102  	if id == "" {
   103  		return "", ErrInvalidCredentials
   104  	}
   105  
   106  	// Create a new token object, specifying signing method and the claims
   107  	// you would like it to contain.
   108  	token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
   109  		"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, s.tz).Unix(),
   110  		"id":  id,
   111  	})
   112  
   113  	// Sign and get the complete encoded token as a string using the secret
   114  	tokenString, err := token.SignedString(s.signKey)
   115  	if err != nil {
   116  		return "", err
   117  	}
   118  
   119  	return tokenString, nil
   120  }
   121  
   122  func (s *service) Authenticate(tokenStr string) (string, error) {
   123  	token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
   124  		// since we only use the one private key to sign the tokens,
   125  		// we also only use its public counter part to verify
   126  		return s.verifyKey, nil
   127  	})
   128  	switch err.(type) {
   129  
   130  	case nil: // no error
   131  
   132  		if !token.Valid { // but may still be invalid
   133  			return "", nil
   134  		}
   135  
   136  		claims, ok := token.Claims.(jwt.MapClaims)
   137  		if !ok {
   138  			return "", nil
   139  		}
   140  
   141  		id, ok := claims["id"].(string)
   142  		if !ok {
   143  			return "", nil
   144  		}
   145  		return id, nil
   146  	case *jwt.ValidationError: // something was wrong during the validation
   147  		return "", nil
   148  
   149  	default: // something else went wrong
   150  		return "", err
   151  	}
   152  }
   153  
   154  // ErrUnauthorized indicates an unauthorized action.
   155  var ErrUnauthorized = xerror.AuthError{Err: fmt.Errorf("unauthorized")}
   156  
   157  func (s *service) Authorize(ctx context.Context, id string) (Role, error) {
   158  	return s.authorizer.Role(ctx, id)
   159  }