At the end of the day, whatever other dark magic is going on in our architecture, it will come down to some Go method being called, doing some work, and returning a result. So the next thing we are going to do is define and implement the Vault service itself.
Inside the vault
folder, add the following code to a new service.go
file:
// Service provides password hashing capabilities. type Service interface { Hash(ctx context.Context, password string) (string, error) Validate(ctx context.Context, password, hash string) (bool, error) }
This interface defines the service.
We define our two methods: Hash
and Validate
. Each takes context.Context
as the first argument, followed by normal string
arguments. The responses are normal Go types as well: string
, bool
, and error
.
Part of designing micro-services is being careful about where state is stored. Even though you will implement the methods of a service in a single file, with access to global variables, you should never use them to store the per-request or even per-service state. It's important to remember that each service is likely to be running on many physical machines multiple times, each with no access to the others' global variables.
In this spirit, we are going to implement our service using an empty struct
, essentially a neat idiomatic Go trick to group methods together in order to implement an interface without storing any state in the object itself. To service.go
, add the following struct
:
type vaultService struct{}
Where possible, starting by writing test code has many advantages that usually end up increasing the quality and maintainability of your code. We are going to write a unit test that will use our new service to hash and then validate a password.
Create a new file called service_test.go
and add the following code:
package vault import ( "testing" "golang.org/x/net/context" ) func TestHasherService(t *testing.T) { srv := NewService() ctx := context.Background() h, err := srv.Hash(ctx, "password") if err != nil { t.Errorf("Hash: %s", err) } ok, err := srv.Validate(ctx, "password", h) if err != nil { t.Errorf("Valid: %s", err) } if !ok { t.Error("expected true from Valid") } ok, err = srv.Validate(ctx, "wrong password", h) if err != nil { t.Errorf("Valid: %s", err) } if ok { t.Error("expected false from Valid") } }
We will create a new service via the NewService
method and then use it to call the Hash
and Validate
methods. We even test an unhappy case, where we get the password wrong and ensure that Validate
returns false
–otherwise, it wouldn't be very secure at all.
A constructor in other object-oriented languages is a special kind of function that creates instances of classes. It performs any initialization and takes in required arguments such as dependencies, among others. It is usually the only way to create an object in these languages, but it often has weird syntax or relies on naming conventions (such as the function name being the same as the class, for example).
Go doesn't have constructors; it's much simpler and just has functions, and since functions can return arguments, a constructor would just be a global function that returns a usable instance of a struct. The Go philosophy of simplicity drives these kinds of decisions for the language designers; rather than forcing people to have to learn about a new concept of constructing objects, developers only have to learn how functions work and they can build constructors with them.
Even if we aren't doing any special work in the construction of an object (such as initializing fields, validating dependencies, and so on), it is sometimes worth adding a construction function anyway. In our case, we do not want to bloat the API by exposing the vaultService
type since we already have our Service
interface type exposed and are hiding it inside a constructor is a nice way to achieve this.
Underneath the vaultService
struct definition, add the NewService
function:
// NewService makes a new Service. func NewService() Service { return vaultService{} }
Not only does this prevent us from needing to expose our internals, but if in the future we do need to do more work to prepare the vaultService
for use, we can also do it without changing the API and, therefore, without requiring the users of our package to change anything on their end, which is a big win for API design.
The first method we will implement in our service is Hash
. It will take a password and generate a hash. The resulting hash can then be passed (along with a password) to the Validate
method later, which will either confirm or deny that the password is correct.
To learn more about the correct way to store passwords in applications, check out the Coda Hale blog post on the subject at https://codahale.com/how-to-safely-store-a-password/.
The point of our service is to ensure that passwords never need to be stored in a database, since that's a security risk if anyone is ever able to get unauthorized access to the database. Instead, you can generate a one-way hash (it cannot be decoded) that can safely be stored, and when users attempt to authenticate, you can perform a check to see whether the password generates the same hash or not. If the hashes match, the passwords are the same; otherwise, they are not.
The bcrypt
package provides methods that do this work for us in a secure and trustworthy way.
To service.go
, add the Hash
method:
func (vaultService) Hash(ctx context.Context, password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hash), nil }
Ensure that you import the appropriate bcrypt
package (try golang.org/x/crypto/bcrypt
). We are essentially wrapping the GenerateFromPassword
function to generate the hash, which we then return provided no errors occurred.
Note that the receiver in the Hash
method is just (vaultService)
; we don't capture the variable because there is no way we can store state on an empty struct
.
Next up, let's add the Validate
method:
func (vaultService) Validate(ctx context.Context, password, hash string) (bool, error) { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) if err != nil { return false, nil } return true, nil }
Similar to Hash
, we are calling bcrypt.CompareHashAndPassword
to determine (in a secure way) whether the password is correct or not. If an error is returned, it means that something is amiss and we return false
indicating that. Otherwise, we return true
when the password is valid.
18.191.176.194