This text is the second part of a series of posts about the Encore.go framework :
- Creating an API with a database
- Creating an API with authentication (<— you are here)
- Communication via Pub/Sub
- Deploy
In the first part, we created a simple API that validates a user given the correct parameters. Now, let's use this functionality to increase the project's complexity: add a new API that requires authentication to be accessed.
The first step is to create a new directory to organize the code:
mkdir feedback
cd feedback
touch api.go
The first version of api.go
looked like this:
package feedback
import (
"context"
)
// API defines the API for the user service
// encore: service
type API struct {
}
// StoreFeedbackParams represents the response of the StoreFeedback function
type StoreFeedbackParams struct {
Title string `json:"title"`
Body string `json:"body"`
}
// StoreFeedbackResponse represents the response of the StoreFeedback function
type StoreFeedbackResponse struct {
ID string `json:"id"`
}
// StoreFeedback stores feedback
//
//encore:api auth method=POST path=/v1/feedback
func (a *API) StoreFeedback(ctx context.Context, p *StoreFeedbackParams) (*StoreFeedbackResponse, error) {
return &StoreFeedbackResponse{ID: ""}, nil
}
What's new is the change we made to Encore's API definition:
//encore:api auth method=POST path=/v1/feedback
According to the documentation, there are three possible configurations for the access level of an API:
-
//encore:api public
– defines a public API that anybody on the internet can call. -
//encore:api private
– defines a private API never accessible to the outside world. It can only be called from other services in your app and via cron jobs. -
//encore:api auth
– defines a public API that anybody can call but requires valid authentication.
Since we have configured our API with the access level auth
, we need to create the logic responsible for this validation. To do this, we will create a new package:
mkdir authentication
touch authentication/handler.go
The code of authentication/handler.go
is:
package authentication
import (
"context"
"encore.app/user"
"encore.dev/beta/auth"
"encore.dev/beta/errs"
"github.com/google/uuid"
)
// Data is the auth data
type Data struct {
Email string
}
// AuthHandler handle auth information
//
//encore:authhandler
func AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) {
if token == "" {
return "", nil, &errs.Error{
Code: errs.Unauthenticated,
Message: "invalid token",
}
}
resp, err := user.ValidateToken(ctx, &user.ValidateTokenParams{Token: token})
if err != nil {
return "", nil, &errs.Error{
Code: errs.Unauthenticated,
Message: "invalid token",
}
}
return auth.UID(uuid.New().String()), &Data{Email: resp.Email}, nil
}
The annotation //encore:authhandler
indicates to the framework that it needs to execute this code whenever an API requires authentication to be accessed. The framework will automatically attempt to access a token that must be sent in the request using the Authorization
header and pass this information as a parameter ( token ) to the AuthHandler function (we can choose another name since what matters is the annotation ). The documentation mentions that more advanced authentication settings, such as other variables and cookies, can be configured.
The function is required to return a value for auth.UID
and can optionally return more data, as I did in this example.
Now we can change our API so that it makes use of the authentication data:
// StoreFeedback stores feedback
//
//encore:api auth method=POST path=/v1/feedback
func (a *API) StoreFeedback(ctx context.Context, p *StoreFeedbackParams) (*StoreFeedbackResponse, error) {
eb := errs.B().Meta("store_feedback", p.Title)
var email string
data := auth.Data()
if data != nil {
email = data.(*authentication.Data).Email
}
if email == "" {
return nil, eb.Code(errs.Unauthenticated).Msg("unauthenticated").Err()
}
f := &Feedback{
Email: email,
Title: p.Title,
Body: p.Body,
}
id, err := a.Service.Store(ctx, f)
if err != nil {
return nil, eb.Code(errs.Internal).Msg("internal error").Err()
}
return &StoreFeedbackResponse{ID: id}, nil
}
The following example shows how to invoke the API with a token generated by the API I developed in the last post:
curl '127.0.0.1:4000/v1/feedback' \
-H 'Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImVtaW5ldHRvQGVtYWlsLmNvbSIsImV4cCI6MTc0MDIzNTcyMiwiaWF0IjoxNzQwMjMyMDkyLCJuYmYiOjE3NDAyMzIwOTJ9._7BZwT3rveDV8gN9f2pBCy1D6_ZA17uRKIOAd7GVKLU' \
-d '{"title":"title","body":"body of feedback"}'
We can also write a test to cover this functionality:
package feedback_test
import (
"context"
"encore.app/authentication"
"encore.app/feedback"
"encore.dev/et"
"github.com/google/uuid"
"testing"
)
type ServiceMock struct{}
func (s *ServiceMock) Store(ctx context.Context, f *feedback.Feedback) (string, error) {
return uuid.New().String(), nil
}
func TestStoreFeedback(t *testing.T) {
api := feedback.API{
Service: &ServiceMock{},
}
et.OverrideAuthInfo("uuid", &authentication.Data{Email: "eminetto@email.com"})
p := feedback.StoreFeedbackParams{
Title: "title",
Body: "body",
}
resp, err := api.StoreFeedback(context.Background(), &p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.ID == "" {
t.Fatalf("expected ID to be non-empty")
}
}
The critical part is the use of the function et.OverrideAuthInfo("uuid", &authentication.Data{Email: "eminetto@email.com"})
, which is a facility that the framework provides for writing tests.
You can see the complete code for this functionality in this repository.
Conclusion
I really liked this feature because it is something common and potentially repetitive, so it is valuable that the framework helps in this process. You can see more complex examples in the documentation, with integration with authentication services such as Auth0.
Another aspect that I found very interesting goes beyond the code. When I was writing the code for the test I presented here, I had doubts about how to pass the authentication data. After reading the documentation and not finding the solution, I joined the project's Discord and asked a question about the subject. On a Saturday morning, in less than 30 minutes, a member of the framework team helped me solve the problem. Extra points for responsiveness and kindness, but here is a suggestion for a review of the documentation to include an example of how to implement this test.
I'm still excited about the framework and the series's following text. What about you, dear reader? What do you think of Encore?
Originally published at https://eltonminetto.dev on Feb 22, 2025
Top comments (0)