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 }