DEV Community

Elton Minetto
Elton Minetto

Posted on

Creating an API with authentication using Encore.go

#go

This text is the second part of a series of posts about the Encore.go framework :

  1. Creating an API with a database
  2. Creating an API with authentication (<— you are here)
  3. Communication via Pub/Sub
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

What's new is the change we made to Encore's API definition:

//encore:api auth method=POST path=/v1/feedback
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

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")
    }
}

Enter fullscreen mode Exit fullscreen mode

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)