User authentication in software development is a big topic. There are many ways to have a user authenticated in an application, and they vary in complexity based on the system's needs. It is a very important aspect of the application being one of the biggest security issues a system might have.
We can decide to have the regular "username" and "password" approach, but that means that our application will need to store the passwords in a database. These passwords need to be encrypted, so the credentials are not accessible in case of a data leak. This model also requires a user registration feature, which usually requires an account confirmation via email or another kind of message. One way to avoid the implementation of all this is to use OAuth 2.0.
OAuth (Open Authorization) is an open standard for access delegation. It allows users to use their credentials of a third-party application service to grant access to our system securely without the need to store passwords.
Replacing authentication is not the only use case for OAuth. Maybe our system has the traditional username and password method, but we want to provide an alternative way of registering and signing in. Oauth 2.0 is an option to allow the users to choose which provider they want to use to authenticate. OAuth 2.0 is implemented by many providers, such as Google, GitHub, Facebook, Twitter, among others. Allowing users to use their third-party accounts to authenticate is a very common usage of the OAuth 2.0 method.
Another situation where OAuth is useful is when our system needs access to the third-party application to do things on behalf of the user. When the users authenticate using OAuth 2.0, they can grant our application some pre-defined permissions allowing our system to access their account through the provider API. This makes it possible, for example, to publish posts on Facebook, read the repositories on Github, or manage YouTube videos, everything accordingly to the permissions granted by the users.
This post focuses on using OAuth 2.0 to replace the application sign-in / sign-up using GitHub as the Oauth provider. The examples are implemented using the Go programming language.
The Oauth 2.0 flow involves three actors: the user that is trying to authenticate, the application that the user is trying to authenticate to, and the provider that takes care of authenticating the user granting access to the application.
The authentication flow is the following:
After being authenticated in the provider, we can get the user's email address or internal ID, and find or create a user record in our system. With our internal system's user, we can put it in a session using JWT, for example.
As always, we are using Clean Architecture concepts to implement the feature. I have written about it in the post Applying Clean Architecture in Go.
The implementation consists of two use cases. The "Request Oauth 2.0" use case, starts with the user accessing the "/login/github" endpoint and ends with the user being redirected to the GitHub authentication page. The "Confirm Oauth 2.0" use case, starts with the user accessing the "/login/github/confirm" endpoint by being redirected from the GitHub page and ends with a session token being saved on the cookies.
The full implementation of a use case consists of the following parts or layers: Use case, entities, ports, adapters, UI. The use case
is a module that contains the high-level policy of the feature. It orchestrates the other layers making them work together. This module is also known as "Service Layer" or "Application Layer" in other architectural patterns. Entities
are managed by the use case and sometimes passed to and returned by the adapters, these entities can be simple data structures or more complex objects, they contain only pure business rules and are not aware of the application-specific business rules which are implemented in the use case layer. The use case interacts with external dependencies through ports
that are expressed as interfaces in code. On these ports adapters
are plugged in. These adapters implement the ports interfaces and adapt the external dependencies to our application's use cases.
We first start with the RequestOAuth2UseCase
. You can see the implementation below:
// internal/core/auth/request_oauth_2_use_case.go package auth import "fmt" type RequestOAuth2UseCase struct { provider OAuth2Provider idGen IDGenerator stateRepo StateRepo } func NewRequestOAuth2UseCase( provider OAuth2Provider, idGen IDGenerator, stateRepo StateRepo, ) *RequestOAuth2UseCase { return &RequestOAuth2UseCase{ provider: provider, idGen: idGen, stateRepo: stateRepo, } } func (u *RequestOAuth2UseCase) Run() (string, error) { state := u.idGen.Generate() err := u.stateRepo.AddState(state) if err != nil { return "", fmt.Errorf("error saving state on RequestOAuth2UseCase: %w", err) } return u.provider.AuthURL(state), nil }
This struct holds the high-level policy for requesting the authentication, the first step of the OAuth 2.0. The Run
method first generates a state using an IDGenerator
. Then this state is stored for later using a StateRepo
. Finally, it requests and returns the authentication URL from an Auth2Provider
. This is the URL for the user to be redirected to authenticate.
The three dependencies of this use case are interfaces defined in the ports.go
file:
// internal/core/auth/ports.go package auth type OAuth2Provider interface { AuthURL(state string) string } type IDGenerator interface { Generate() string } type StateRepo interface { AddState(state string) error }
For each one of these interfaces, there is an adapter. Each interface is an abstraction and each adapter is concrete. Meaning we can replace the concrete implementation without changing the high-level rules of the use case.
For each one of the ports, we have a concrete adapter implementation. For the OAuth2Provider
. We have the github.Provider
adapter. For the IDGenerator
, the uuid.Generator
concrete implementation. And for the StateRepo
, we have the memory.StateRepo
.
Here is the implementation of the github.Provider
module:
// internal/adapters/oauth2provider/github/provider.go package github import ( "golang.org/x/oauth2" "golang.org/x/oauth2/github" ) type Provider struct { config oauth2.Config } func NewProvider(clientID, clientSecret string) *Provider { config := oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: github.Endpoint, } return &Provider{config: config} } func (p *Provider) AuthURL(state string) string { return p.config.AuthCodeURL(state) }
Go has support to OAuth 2.0 using the package golang.org/x/oauth2
. This package abstracts away how to handle OAuth 2.0 with different providers, we only need to initialize the oauth2.Config properly. Part of this package is also several provider Endpoints available to use. We are using the GitHub implementation in this example. You can see a list of all the providers in the oauth package documentation.
The NewProvider
function receives a clientID
and a clientSecret
and creates an instance of the oauth2.Config
with those values and the GitHub endpoints. The AuthURL
method simply delegates to the config to generate the URL for the user to be redirected.
The next adapter is the uuid.Generator
which implements the IDGenerator
interface:
// internal/adapters/idgenerator/uuid/uuid.go package uuid import "github.com/google/uuid" type Generator struct{} func NewGenerator() *Generator { return &Generator{} } func (g *Generator) Generate() string { return uuid.New().String() }
This package uses the github.com/google/uuid
library. The Generate
method generates a UUID version 4 and returns its string format.
The last adapter for this first use case is the memory.StateRepo
which implements the StateRepo
interface:
// internal/adapters/staterepo/memory/state_repo.go package memory type StateRepo struct { states map[string]bool } func NewStateRepo() *StateRepo { return &StateRepo{ states: make(map[string]bool), } } func (r *StateRepo) AddState(state string) error { r.states[state] = true return nil }
This is an in-memory adapter for the StateRepo
port. It stores all the states on a map that will be checked for existence later. As a state value doesn't live for too long, an in-memory version of this adapter can work in production, as long as the application is not scaled horizontally, and there are no frequent restarts due to deployments. A more stable version would be using a database like Redis or Postgres to store the states. For this example, the in-memory version is good enough.
Having all the adapters implemented, let's move to the UI layer handling the HTTP requests. For that we need an HTTP Handler that invokes the use case:
// internal/ui/web/request_oauth_2_handler.go package web import "net/http" type RequestOAuth2Handler struct { usecase RequestOAuth2UseCase template *TemplateRenderer } func NewRequestOAuth2Handler(usecase RequestOAuth2UseCase, template *TemplateRenderer) *RequestOAuth2Handler { return &RequestOAuth2Handler{usecase: usecase, template: template} } func (h *RequestOAuth2Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { redirectURL, err := h.usecase.Run() if err != nil { w.WriteHeader(http.StatusInternalServerError) h.template.Render(w, "500.html", nil) return } http.Redirect(w, r, redirectURL, http.StatusSeeOther) }
The RequestOAuth2Handler
receives the RequestOAuth2UseCase
as a dependency. the ServeHTTP
method calls the Run
method from the use case retrieving the redirect URL. In case of error, it responds with a status code 500 and renders an error template. If there is no error it redirects the user to the retrieved redirect URL.
The router simply registers this handler to the /login/github
path:
// internal/ui/web/router.go package web import ( "net/http" ) func NewRouter(templatePath, staticFilesPath string, usecases *UseCases, baseURL string) http.Handler { templateRenderer := NewTemplateRenderer(templatePath, baseURL) mux := http.NewServeMux() // ... mux.Handle("/login/github", NewRequestOAuth2Handler(usecases.RequestOAuth2, templateRenderer)) return mux }
That ends the "Request OAuth 2.0" use case. By accessing the "/login/github" endpoint, users get redirected to the GitHub authentication page where they give permissions to our app to get their information from GitHub. After that, these users get redirected back to our application where the second use case starts.
The "Confirm OAuth 2.0" use case starts when the OAuth 2.0 provider redirects the user back to our application after the proper permissions were granted by this same user. Again the full implementation of this use case consists of the layers mentioned before: Use case, entities, ports, adapters, and UI.
After the users authenticate on GitHub and provide access to our application, they get redirected to the configured redirect URL on GitHub. For this example, we are using the "/login/github/confirm" endpoint for that. To implement it we start with the ConfirmOAuth2UseCase
. As this is a big module, we are breaking the implementation into parts:
// internal/core/auth/confirm_oauth_2_use_case.go package auth import ( "context" "errors" "fmt" "time" ) type ConfirmOAuth2UseCase struct { provider OAuth2Provider stateRepo StateRepo userRepo UserRepo idGen IDGenerator tokenEncoder TokenEncoder } func NewConfirmOAuth2UseCase( provider OAuth2Provider, stateRepo StateRepo, userRepo UserRepo, idGen IDGenerator, tokenEncoder TokenEncoder, ) *ConfirmOAuth2UseCase { return &ConfirmOAuth2UseCase{ provider: provider, stateRepo: stateRepo, userRepo: userRepo, idGen: idGen, tokenEncoder: tokenEncoder, } } // ...
We start with the struct definition and the constructor function. Here we can see that this use case has several dependencies. The Auth2Provider
and the StateRepo
are the same mentioned in the previous use case and are used here to validate the state and authenticate the user in the provider. The IDGenerator
and the UserRepo
are used to create or update the user representation in our system. Finally, the TokenEncoder
is used to create a session token to authenticate the user.
Now that we have the use case struct let's see the Run
method implementation:
// internal/core/auth/confirm_oauth_2_use_case.go // ... func (u *ConfirmOAuth2UseCase) Run(ctx context.Context, state, code string) (string, error) { providerUser, err := u.processOAuth2Authentication(ctx, state, code) if err != nil { return "", err } return u.resolveUserAndGetToken(providerUser) } // ...
As the entry point of the use case, the Run
method splits the flow into two private (or unexported in Go terms) method calls. The processOAuth2Authentication
handles the communication with the OAuth 2.0 provider and the resolveUserAndGetToken
method manages the user creation and authentication in our application based on the OAuth 2.0 response. You can see the implementation of the first flow next:
// internal/core/auth/confirm_oauth_2_use_case.go // ... func (u *ConfirmOAuth2UseCase) processOAuth2Authentication(ctx context.Context, state, code string) (ProviderUser, error) { err := u.validateAndRemoveState(state) if err != nil { return ProviderUser{}, err } return u.getProviderAuthenticatedUser(ctx, code) } func (u *ConfirmOAuth2UseCase) validateAndRemoveState(state string) error { exists, err := u.stateRepo.Exists(state) if err != nil { return fmt.Errorf("error checking state on ConfirmOAuth2UseCase: %w", err) } if !exists { return ErrInvalidState } err = u.stateRepo.Remove(state) if err != nil { return fmt.Errorf("error authenticating user on ConfirmOAuth2UseCase: %w", err) } return nil } func (u *ConfirmOAuth2UseCase) getProviderAuthenticatedUser(ctx context.Context, code string) (ProviderUser, error) { providerUser, err := u.provider.AuthenticatedUser(ctx, code) if err != nil { return ProviderUser{}, fmt.Errorf("error authenticating user on ConfirmOAuth2UseCase: %w", err) } return providerUser, nil } // ...
The processOAuth2Authentication
method splits the flow again into two private methods: validateAndRemoveState
and getProviderAuthenticatedUser
.
The validateAndRemoveState
method checks if the state exists using the StateRepo
dependency and, if it does, it is removed from the repository not allowing this confirmation to happen more than once. This state is the random string generated in the first use case "Request OAuth 2.0" that was sent to the provider and returned as an argument when the user got redirected back to our system.
Next is the getProviderAuthenticatedUser
method. Here it gets the authenticated user from the OAuth2Provider
dependency passing a context and a code. This code, like the state, also came from the provider in the redirection to our application. The getProviderAuthenticatedUser
returns a ProviderUser
which contains the user information needed to proceed with the authentication.
Going back to the Run
method, having the ProviderUser
value, it calls the resolveUserAndGetToken
method:
// internal/core/auth/confirm_oauth_2_use_case.go // ... func (u *ConfirmOAuth2UseCase) resolveUserAndGetToken(providerUser ProviderUser) (string, error) { user, err := u.createOrUpdateUser(providerUser) if err != nil { return "", err } return u.getAuthenticationToken(user) } // ...
This method splits the flow again into two private methods: createOrUpdateUser
and getAuthenticationToken
.
// internal/core/auth/confirm_oauth_2_use_case.go // ... func (u *ConfirmOAuth2UseCase) createOrUpdateUser(providerUser ProviderUser) (User, error) { user, err := u.userRepo.FindUserByProviderUserID(providerUser.ID) if errors.Is(err, ErrUserNotFound) { return u.createNewUser(providerUser) } if err != nil { return User{}, fmt.Errorf("error finding user on ConfirmOAuth2UseCase: %w", err) } return u.updateExistingUser(user, providerUser) } func (u *ConfirmOAuth2UseCase) createNewUser(providerUser ProviderUser) (User, error) { user := User{ ID: u.idGen.Generate(), ProviderUserID: providerUser.ID, Email: providerUser.Email, Name: providerUser.Name, AvatarURL: providerUser.AvatarURL, } err := u.userRepo.CreateUser(user) if err != nil { return User{}, fmt.Errorf("error creatinng user on ConfirmOAuth2UseCase: %w", err) } return user, nil } func (u *ConfirmOAuth2UseCase) updateExistingUser(user User, providerUser ProviderUser) (User, error) { user.Email = providerUser.Email user.Name = providerUser.Name user.AvatarURL = providerUser.AvatarURL err := u.userRepo.UpdateUser(user) if err != nil { return User{}, fmt.Errorf("error updating user on ConfirmOAuth2UseCase: %w", err) } return user, nil } // ...
The createOrUpdateUser
method uses the UserRepo
dependency to check if the user already exists. It does that by trying to find a user by the ProviderUserID
field. This field is the internal user ID in the OAuth 2.0 provider.
If the user does not exist, it creates a new user by calling the createNewUser
method, which will again use the UserRepo
to persist the new user. This new user is created with an ID, generated by the IDGenerator
dependency, and the email, name, and avatar URL provided by the OAuth 2.0 provider. Additionally, it also persists the ProviderUserID
field.
If a user exists, updateExistingUser
is called. This method updates the user email, name, and avatar URL to have the information always up to date when users login into our application.
Going back to the resolveUserAndGetToken
method, the next method called is the getAuthenticationToken
:
// internal/core/auth/confirm_oauth_2_use_case.go // ... const TokenExpiration = 24 * time.Hour func (u *ConfirmOAuth2UseCase) getAuthenticationToken(user User) (string, error) { token, err := u.tokenEncoder.Encode(user.ID, TokenExpiration) if err != nil { return "", fmt.Errorf("error encoding token on ConfirmOAuth2UseCase: %w", err) } return token, nil }
This method uses the TokenEncoder
dependency to encode the ID of the user in a token. This token is also the response of the use case and is the session token to be kept in the browser cookies as it contains the identity of the authenticated user.
As we can see, the ConfirmOAuth2UseCase
type has multiple responsibilities. Each time it splits the flow into other methods could potentially be extracted into another type and facilitate code reuse. For example, if we need to authenticate a user using a method other than OAuth 2.0, the getAuthenticationToken
method could become a standalone type. If we need to process the OAuth 2.0 confirmation but not proceed to the user creation and authentication, the getProviderAuthenticatedUser
method could become another standalone type. For the moment, the current implementation is sufficient as these functionalities are only used in this module and we don't know yet what could potentially be reused in the future.
You can see the full implementation of ConfirmOAuth2UseCase
on GitHub.
For this new use case, we need to extend some of the interface dependencies and add others. The interfaces changes are shown below:
// internal/core/auth/ports.go package auth import ( "context" "time" ) type OAuth2Provider interface { AuthURL(state string) string AuthenticatedUser(ctx context.Context, code string) (ProviderUser, error) } type IDGenerator interface { Generate() string } type StateRepo interface { AddState(state string) error Exists(state string) (bool, error) Remove(state string) error } type UserRepo interface { CreateUser(user User) error UpdateUser(user User) error FindUserByProviderUserID(providerUserID string) (User, error) } type TokenEncoder interface { Encode(value string, expiresIn time.Duration) (string, error) }
The OAuth2Provider
has a new method AuthenticatedUser
. IdGenerator
is just reused without modification. On the StateRepo
, two methods were added: Exists
and Remove
. And two new interfaces were added: UserRepo
and TokenEncoder
.
The Oauth2Provider
now returns a ProviderUser
and the UserRepo
receives and returns instances of User
. These entities are defined below:
// internal/core/auth/entities.go package auth import "errors" type ProviderUser struct { ID string Email string Name string AvatarURL string } type User struct { ID string ProviderUserID string Email string Name string AvatarURL string } var ErrInvalidState = errors.New("invalid state error") var ErrUserNotFound = errors.New("user not found") var ErrTokenExpired = errors.New("token expired")
These two entities, although very similar, correspond to two different things. The ProviderUser
structs is a representation of the user returned by the provider. The User
struct is a representation of the internal user of our system. Although they are very similar, they can change for different reasons, so they should be separated. This file also defines the errors that are returned by the adapters and used by the use case.
Now let's go to the implementation of the adapters. Let's start with the new method in the OAuth2Provider
:
// internal/adapters/oauth2provider/github/provider.go package github import ( "context" "fmt" "net/http" "github.com/geisonbiazus/blog/internal/core/auth" "golang.org/x/oauth2" "golang.org/x/oauth2/github" ) type Provider struct { config oauth2.Config } func NewProvider(clientID, clientSecret string) *Provider { config := oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: github.Endpoint, } return &Provider{config: config} } // ... func (p *Provider) AuthenticatedUser(ctx context.Context, code string) (auth.ProviderUser, error) { httpClient, err := p.exchangeTokenAndGetClient(ctx, code) if err != nil { return auth.ProviderUser{}, err } return NewClient(httpClient).GetAuthenticatedUser() } func (p *Provider) exchangeTokenAndGetClient(ctx context.Context, code string) (*http.Client, error) { token, err := p.config.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("error exchanging token on github. Provider: %w", err) } tokenSource := p.config.TokenSource(ctx, token) return oauth2.NewClient(ctx, tokenSource), nil }
The AuthenticateUser
method calls the private method exchangeTokenAndGetClient
. This method receives a context and a code and returns an instance of http.Client
that is already authenticated on the GitHub API. The authentication on GitHub is done using golang.org/x/oauth2
library. Using our instance of the oauth2.Config
, the Exchange
method is called returning an instance of the oauth2.Token
type.
The oauth2.Token
contains both an access token and a refresh token. It is serializable to JSON and can be saved in case our application needs to keep the connection to the provider persistent. So, for example, in the use case of authorizing our application to have access to GitHub to continuously collect metrics from the users' repositories, the users would give our application access to their GitHub account only once and the subsequent authentications would be done by either using the access token or the refresh token to get a new access token.
The oauth2.Token
is used to get an instance of a auth2.TokenSource
from the oauth2.Config
instance, which is passed to the oauth2.NewClient
method returning an http.Client
. This client is already authenticated to the GitHub API and it also automatically refreshes the access token in case it expires.
Back to the AuthenticatedUser
method, with the authenticated HTTP client in hand, the github.NewClient
method is used to create a github.Client
instance and then request the authenticated user information from GitHub using the GetAuthenticatedUser
method.
You can see the github.Client
implementation below:
// internal/adapters/oauth2provider/github/client.go package github import ( "encoding/json" "fmt" "io/ioutil" "net/http" "strconv" "github.com/geisonbiazus/blog/internal/core/auth" ) type HTTPClient interface { Get(url string) (resp *http.Response, err error) } type Client struct { httpClient HTTPClient } func NewClient(httpClient HTTPClient) *Client { return &Client{httpClient: httpClient} } func (c *Client) GetAuthenticatedUser() (auth.ProviderUser, error) { resp, err := c.requestCurrentUser() if err != nil { return auth.ProviderUser{}, err } defer resp.Body.Close() return c.parseResponse(resp) } func (c *Client) requestCurrentUser() (*http.Response, error) { resp, err := c.httpClient.Get("https://api.github.com/user") if err != nil { return nil, fmt.Errorf("error requesting user on github.Client: %w", err) } return resp, nil } func (c *Client) parseResponse(resp *http.Response) (auth.ProviderUser, error) { if resp.StatusCode != http.StatusOK { return c.errorResponse(resp) } user, err := c.decodeResponseBody(resp) if err != nil { return auth.ProviderUser{}, err } return auth.ProviderUser{ ID: strconv.Itoa(user.ID), AvatarURL: user.AvatarURL, Name: user.Name, Email: user.Email, }, nil } func (c *Client) errorResponse(resp *http.Response) (auth.ProviderUser, error) { body, _ := ioutil.ReadAll(resp.Body) err := fmt.Errorf("error requesting user. Status: %d. Response: %s", resp.StatusCode, body) return auth.ProviderUser{}, err } func (c *Client) decodeResponseBody(resp *http.Response) (*githubUser, error) { user := &githubUser{} err := json.NewDecoder(resp.Body).Decode(user) if err != nil { return nil, fmt.Errorf("error decoding JSON on github.Client: %w", err) } return user, nil } type githubUser struct { ID int `json:"id"` AvatarURL string `json:"avatar_url"` Type string `json:"type"` Name string `json:"name"` Email string `json:"email"` }
The github.Client
type uses has one public method called GetAuthenticatedUser
that uses the GitHub REST API to get the authenticated user information. The API returns A JSON representation of the user, which is decoded and then used to build an auth.ProviderUser
.
The StateRepo
adapter needs to be enhanced with the Exists
and Remove
methods to satisfy the "Request Oauth 2.0" use case:
// internal/adapters/staterepo/memory/state_repo.go package memory type StateRepo struct { states map[string]bool } func NewStateRepo() *StateRepo { return &StateRepo{ states: make(map[string]bool), } } func (r *StateRepo) AddState(state string) error { r.states[state] = true return nil } // New methods bellow func (r *StateRepo) Exists(state string) (bool, error) { _, ok := r.states[state] return ok, nil } func (r *StateRepo) Remove(state string) error { delete(r.states, state) return nil }
The UserRepo
adapter, like the StateRepo
, uses an in-memory implementation for this post. Even though an "in-memory" implementation works well for testing the application, its data is lost every time the application is restarted, so for production, it is required a database or other kind of persistence implementation. As this is not the focus of this post, I'll display only the in-memory version here. You can see the implementation below:
// internal/adapters/userrepo/memory/user_repo.go package memory import ( "github.com/geisonbiazus/blog/internal/core/auth" ) type UserRepo struct { users []auth.User } func NewUserRepo() *UserRepo { return &UserRepo{users: []auth.User{}} } func (r *UserRepo) CreateUser(user auth.User) error { r.users = append(r.users, user) return nil } func (r *UserRepo) UpdateUser(user auth.User) error { for i, existingUser := range r.users { if existingUser.ID == user.ID { r.users[i] = user return nil } } return auth.ErrUserNotFound } func (r *UserRepo) FindUserByProviderUserID(providerUserID string) (auth.User, error) { for _, user := range r.users { if user.ProviderUserID == providerUserID { return user, nil } } return auth.User{}, auth.ErrUserNotFound }
The last adapter is the TokenEncoder
. For this adapter, we have a JWT implementation to sign and encode the user reference to be kept in the user session. You can see the implementation below:
// internal/adapters/tokenencoder/jwt/jwt.go package jwt import ( "errors" "fmt" "strings" "time" "github.com/dgrijalva/jwt-go" "github.com/geisonbiazus/blog/internal/core/auth" ) type TokenEncoder struct { secret []byte } func NewTokenEncoder(secret string) *TokenEncoder { return &TokenEncoder{secret: []byte(secret)} } func (m *TokenEncoder) Encode(value string, expiresIn time.Duration) (string, error) { claims := newClaims(value, expiresIn) t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) signedToken, err := t.SignedString(m.secret) if err != nil { return "", fmt.Errorf("error signing string on jwt.TokenManager: %w", err) } return signedToken, nil } type jwtClaims struct { jwt.StandardClaims } func newClaims(sub string, expiresIn time.Duration) *jwtClaims { return &jwtClaims{ StandardClaims: jwt.StandardClaims{ Subject: sub, ExpiresAt: expiresAt(expiresIn), }, } } func expiresAt(expiresIn time.Duration) int64 { return time.Now().Add(expiresIn).Unix() }
The jwt.TokenEncoder
uses the github.com/dgrijalva/jwt-go
library to generate and sign the JWT. As in the content of this post we just encode the token, we are omitting the Decode
method. You can see the full implementation on the blog repository on GitHub. You can also learn more about JWT on the JWT website
Having all the adapters implemented, what is missing is an HTTP Handler to call our use case. You can see it below:
// internal/ui/web/confirm_oauth_2_handler.go package web import ( "errors" "net/http" "github.com/geisonbiazus/blog/internal/core/auth" ) type ConfirmOAuth2Handler struct { usecase ConfirmOAuth2UseCase template *TemplateRenderer baseURL string } func NewConfirmOAuth2Handler( usecase ConfirmOAuth2UseCase, templateRenderer *TemplateRenderer, baseURL string, ) *ConfirmOAuth2Handler { return &ConfirmOAuth2Handler{ usecase: usecase, template: templateRenderer, baseURL: baseURL, } } func (h *ConfirmOAuth2Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { state := r.URL.Query().Get("state") code := r.URL.Query().Get("code") token, err := h.usecase.Run(r.Context(), state, code) if err != nil { h.respondWithError(w, err) return } http.SetCookie(w, h.newSessionCookie(token)) http.Redirect(w, r, h.baseURL, http.StatusSeeOther) } func (h *ConfirmOAuth2Handler) respondWithError(w http.ResponseWriter, err error) { if errors.Is(err, auth.ErrInvalidState) { w.WriteHeader(http.StatusNotFound) h.template.Render(w, "404.html", nil) } else { w.WriteHeader(http.StatusInternalServerError) h.template.Render(w, "500.html", nil) } } func (h *ConfirmOAuth2Handler) newSessionCookie(token string) *http.Cookie { return &http.Cookie{ Name: "_blog_session", Value: token, Path: "/", } }
The ConfirmOAuth2Handler
serves the endpoint in which the users will be redirected from the OAuth 2.0 provider. The ServeHTTP
method gets the state and the code from the URL query params and executes the ConfirmOAuth2UseCase
by calling the Run
method. If an error is returned, the appropriate error page is rendered. In case there is no error, it means that the authentication was successful and we have the user token. So this token is set to the browser cookies and the user is redirected to the "/" endpoint.
The handler is registered in the router to the /login/github/confirm
path:
// internal/ui/web/router.go package web import ( "net/http" ) func NewRouter(templatePath, staticFilesPath string, usecases *UseCases, baseURL string) http.Handler { templateRenderer := NewTemplateRenderer(templatePath, baseURL) mux := http.NewServeMux() // ... mux.Handle("/login/github/confirm", NewConfirmOAuth2Handler(usecases.ConfirmOAuth2, templateRenderer, baseURL)) return mux }
That ends the "Confirm OAuth 2.0" use case and the implementation of the OAuth 2.0 authentication. After that, we can get the value of the _blog_session
cookie in the subsequent requests, decode it and use it to get the authenticated user.
OAuth 2.0, being a standard that is followed by many providers, facilitates applications to authenticate and connect to many third-party systems in a secure way. There are many libraries that abstract this authentication process but this is just one portion of the full process, which also consists of managing the users and generating session tokens. The main challenge is to orchestrate this process in a testable and maintainable way, and for that, the Clean Architecture is a good fit.
You can see the full implementation in the Blog Repository on GitHub.