# Authentication
Since v2.5.0# Introduction
Goyave provides a convenient and expandable way of handling authentication in your application. Authentication can be enabled when registering your routes:
import "goyave.dev/goyave/v4/auth"
//...
authenticator := auth.Middleware(&model.User{}, &auth.BasicAuthenticator{})
router.Middleware(authenticator)
Authentication is handled by a simple middleware calling an Authenticator. This middleware also needs a model, which will be used to fetch user information on a successful login.
# auth.Middleware
Middleware create a new authenticator middleware to authenticate the given model using the given authenticator.
Parameters | Return |
---|---|
model interface{} | goyave.Middleware |
authenticator Authenticator |
Example:
authenticator := auth.Middleware(&model.User{}, &auth.BasicAuthenticator{})
router.Middleware(authenticator)
# Authenticators
This section will go into more details about Authenticators and explain the built-in ones. You will also learn how to implement an authenticator yourself.
Authenticator
is a functional interface with a single method accepting a request and a model pointer as parameters.
Authenticate(request *goyave.Request, user interface{}) error
The goal of this function is to check user credentials, most of the time from the request's headers. If they are correct and the user can be authenticated, the user
parameter is updated with the user's information. User information is most of the time fetched from the database.
On the other hand, if the user cannot be authenticated, the Authenticate
method must return an error
containing a localized message. For example, the error could be that the token lifetime is expired, thus "Your authentication token is expired." will be returned.
Authenticators use their model's struct fields tags to know which field to use for username and password. To make your model compatible with authentication, you must add the auth:"username"
and auth:"password"
tags:
type User struct {
gorm.Model
Email string `gorm:"type:char(100);uniqueIndex" auth:"username"`
Name string `gorm:"type:char(100)"`
Password string `gorm:"type:char(60)" auth:"password"`
}
WARNING
- The username should be unique.
- Passwords should be hashed before being stored in the database.
Built-in Goyave Authenticators use bcrypt
(opens new window) to check if a password matches the user request.
When a user is successfully authenticated on a protected route, its information is available in the controller handler, through the request User
field.
func Hello(response *goyave.Response, request *goyave.Request) {
user := request.User.(*model.User)
response.String(http.StatusOK, "Hello " + user.Name)
}
TIP
Remember that Goyave is primarily focused on APIs. It doesn't use session nor cookies in its core features, making requests stateless.
If you want to implement cookie or session-based authentication, be sure to protect your application from CSRF attacks (opens new window).
# Basic Auth
Basic authentication (opens new window) is an authentication method using the Authorization
header and a simple username and password combination with the following format: username:password
, encoded in base64. There are two built-in Authenticators for Basic auth.
# Database provider
This Authenticator fetches the user information from the database, using the field tags explained earlier.
To apply this protection to your routes, add the following middleware:
authenticator := auth.Middleware(&model.User{}, &auth.BasicAuthenticator{})
router.Middleware(authenticator)
You can then try requesting a protected route:
$ curl -u username:password http://localhost:8080/hello
Hello Jérémy
This provider supports the Optional
flag, which defines if the authenticator allows requests that don't provide credentials. Handlers should therefore check if request.User
is not nil
before accessing it.
authenticator := auth.Middleware(&model.User{}, &auth.BasicAuthenticator{Optional: true})
router.Middleware(authenticator)
# Config provider
This Authenticator fetches the user information from the config. This method is good for quick proof-of-concepts, as it requires minimum setup, but shouldn't be used in real-world applications.
- The
auth.basic.username
config entry defines the username that must be matched. - The
auth.basic.password
config entry defines the password that must be matched.
To apply this protection to your routes, start by adding the auth
category at the root of your configuration, and the auth.basic
sub-category:
{
...
"auth": {
"basic": {
"username": "admin",
"password": "admin"
}
}
}
Then, add the following middleware:
router.Middleware(auth.ConfigBasicAuth())
The model used for this Authenticator is auth.BasicUser
:
type BasicUser struct {
Name string
}
You can then try requesting a protected route:
$ curl -u username:password http://localhost:8080/hello
# auth.ConfigBasicAuth
Create a new authenticator middleware for config-based Basic authentication. On auth success, the request user is set to a auth.BasicUser
.
The user is authenticated if the auth.basic.username
and auth.basic.password
config entries match the request's Authorization header.
Parameters | Return |
---|---|
goyave.Middleware |
# JSON Web Token (JWT)
JWT, or JSON Web Token (opens new window), is an open standard of authentication that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. Goyave supports HMAC, RSA (with or without password) and ECDSA. RSA and ECDSA require PEM-encoded keys. Goyave uses the golang-jwt/jwt (opens new window) library in the background.
JWT Authentication comes with the auth.jwt.expiry
configuration entry, which defines the number of seconds a token is valid for and defaults to 300
(5 minutes).
# Basic usage
By default, Goyave's built-in JWT components will use HMAC-SHA256 to sign generated tokens and will expect this method for signature verification. In this basic usage example, this is what we are going to use. Check the next sections for details on how to use RSA or ECDSA.
To apply JWT authentication to your routes, start by adding the auth
category at the root of your configuration, and the auth.jwt
sub-category:
{
...
"auth": {
"jwt": {
"expiry": 300,
"secret": "jwt-secret"
}
}
}
WARNING
Make sure your HMAC secret is securely generated and is long enough:
- HMAC-SHA256: the secret must be 256+ bits long
- HMAC-SHA384: the secret must be 384+ bits long
- HMAC-SHA512: the secret must be 512+ bits long
Then, add the following middleware:
authenticator := auth.Middleware(&model.User{}, &auth.JWTAuthenticator{})
router.Middleware(authenticator)
To request a protected route, you will need to add the following header:
Authorization: Bearer <YOUR_TOKEN>
# Optional authentication
This provider supports the Optional
flag, which defines if the authenticator allows requests that don't provide credentials. Handlers should therefore check if request.User
is not nil
before accessing it.
authenticator := auth.Middleware(&model.User{}, &auth.JWTAuthenticator{Optional: true})
router.Middleware(authenticator)
# Custom ID claim name
By default, auth.JWTAuthenticator
looks for the claim named sub
in the given token. You can customize the name of the token using the ClaimName
field:
authenticator := auth.Middleware(&model.User{}, &auth.JWTAuthenticator{ClaimName: "userid"})
router.Middleware(authenticator)
# Claims in request.Extra
If a token is valid (even if authentication fails), its claims are put into request.Extra
with the jwt_claims
key, so you can access them in any subsequent handler:
import "github.com/golang-jwt/jwt"
//...
func myHandler(resp *Response, r *Request) {
claims := request.Extra["jwt_claims"].(jwt.MapClaims)
//...
}
# RSA
If you expect tokens to be signed with RSA, you will need to add the auth.jwt.rsa.public
configuration entry. This entry defines the path to the PEM-encoded RSA public key file. Make sure the system user running your application has read access. Both absolute and relative paths are supported.
{
...
"auth": {
"jwt": {
"expiry": 300,
"rsa": {
"public": "/path/to/rsa-key.pem"
}
}
}
}
Then, specify the expected signature method in the SignatureMethod
field of auth.JWTAuthenticator
:
import "github.com/golang-jwt/jwt"
//...
authenticator := auth.Middleware(&model.User{}, &auth.JWTAuthenticator{SigningMethod: jwt.SigningMethodRS256})
router.Middleware(authenticator)
TIP
You can find the list of available methods in the jwt-go documentation (opens new window).
- For testing purposes, you can generate an RSA key-pair using OpenSSL:
openssl genrsa -out rsa-private.pem 2048
openssl rsa -in rsa-private.pem -outform PEM -pubout -out rsa-public.pem
# ECDSA
If you expect tokens to be signed with ECDSA, you will need to add the auth.jwt.ecdsa.public
configuration entry. This entry defines the path to the PEM-encoded ECDSA public key file. Make sure the system user running your application has read access. Both absolute and relative paths are supported.
{
...
"auth": {
"jwt": {
"expiry": 300,
"rsa": {
"public": "/path/to/ecdsa-key.pem"
}
}
}
}
Then, specify the expected signature method in the SignatureMethod
field of auth.JWTAuthenticator
:
import "github.com/golang-jwt/jwt"
//...
authenticator := auth.Middleware(&model.User{}, &auth.JWTAuthenticator{SigningMethod: jwt.SigningMethodES256})
router.Middleware(authenticator)
TIP
- You can find the list of available methods in the jwt-go documentation (opens new window).
- For testing purposes, you can generate an ECDSA key-pair using OpenSSL:
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa-private.key
openssl pkcs8 -topk8 -in ecdsa-private.key -out ecdsa-private.pem
openssl ec -in ecdsa-private.pem -pubout -out ecdsa-public.pem
# Generating tokens
# auth.GenerateTokenWithClaims
Generate a new JWT with custom claims and signed using the given signing method.
The token is set to expire in the amount of seconds defined by the auth.jwt.expiry
config entry.
Depending on the given signing method, the following configuration entries will be used:
- RSA:
auth.jwt.rsa.private
: path to the private PEM-encoded RSA key.auth.jwt.rsa.password
: optional password for the private RSA key.
- ECDSA:
auth.jwt.ecdsa.private
: path to the private PEM-encoded ECDSA key. - HMAC:
auth.jwt.secret
: HMAC secret
The generated token will also contain the following claims:
nbf
: "Not before", the current timestamp is usedexp
: "Expiry", the current timestamp plus theauth.jwt.expiry
config entry.
nbf
and exp
can be overridden if they are set in the claims
parameter.
Parameters | Return |
---|---|
claims jwt.MapClaims | string |
signingMethod jwt.SigningMethod | error |
Example:
import "github.com/golang-jwt/jwt"
//...
token, err := auth.GenerateTokenWithClaims(jwt.MapClaims{"sub": user.ID}, jwt.SigningMethodES256)
if err != nil {
panic(err)
}
fmt.Println(token)
# auth.GenerateToken
Generate a new JWT. This function is a shortcut to auth.GenerateTokenWithClaims()
.
The token is created using the HMAC SHA256 method and signed using the auth.jwt.secret
config entry.
The token is set to expire in the amount of seconds defined by the auth.jwt.expiry
config entry.
The generated token will contain the following claims:
userid
: has the value of theid
parameternbf
: "Not before", the current timestamp is usedexp
: "Expiry", the current timestamp plus theauth.jwt.expiry
config entry.
Parameters | Return |
---|---|
id interface{} | string |
error |
Example:
token, err := auth.GenerateToken(user.ID)
if err != nil {
panic(err)
}
fmt.Println(token)
# JWTController
JWTAuthenticator comes with a built-in login controller for password grant, using the field tags explained earlier. You can register the /auth/login
route using the helper function auth.JWTRoutes(router)
.
# auth.JWTRoutes
Create a /auth
route group and registers the POST /auth/login
validated route. Returns the new route group.
Validation rules are as follows:
username
: required stringpassword
: required string
The given model is used for username and password retrieval and for instantiating an authenticated request's user.
Ensure that the given router is not protected by JWT authentication, otherwise your users wouldn't be able to log in.
Parameters | Return |
---|---|
router *goyave.Router | *goyave.Router |
model interface{} |
Example:
func Register(router *goyave.Router) {
auth.JWTRoutes(router, &model.User{})
}
# auth.NewJWTController
If you want or need to register the routes yourself, you can instantiate a new JWTController
using auth.NewJWTController()
.
This function creates a new JWTController
that will be using the given model for login and token generation.
A JWTController
contains one handler called Login
.
Parameters | Return |
---|---|
model interface{} | *auth.JWTController |
Example:
jwtRouter := router.Subrouter("/auth")
jwtRouter.Route("POST", "/login", auth.NewJWTController(&model.User{}).Login).Validate(validation.RuleSet{
"username": validation.List{"required", "string"},
"password": validation.List{"required", "string"},
})
TIP
By default, the controller will use the "username" and "password" fields from incoming requests for the authentication process. This can be changed by modifying the controller's UsernameField
and PasswordField
structure fields:
jwtController := auth.NewJWTController(&model.User{})
jwtController.UsernameField = "email"
jwtController.PasswordField = "pwd"
# Signing method
As JWTController
generates token for you, you can also customize the signing method it uses. By default, HMAC-SHA256 is used. You can override this by changing the SigningMethod
field:
import "github.com/golang-jwt/jwt"
//...
jwtController := auth.NewJWTController(&model.User{})
jwtController.SigningMethod = jwt.SigningMethodES256
# Custom token generation
You can also override the token generation logic executed by the controller on successful authentication by setting the TokenFunc
field:
jwtController := auth.NewJWTController(&model.User{})
jwtController.TokenFunc = func(r *goyave.Request, user interface{}) (string, error) {
return GenerateTokenWithClaims(jwt.MapClaims{
"sub": user.(*model.User).ID,
"name": user.(*model.User).Name,
}, jwt.SigningMethodHS256)
}
TIP
auth.TokenFunc
is an alias for func(request *goyave.Request, user interface{}) (string, error)
# Writing custom Authenticator
The Goyave authentication system is expandable, meaning that you can implement more authentication methods by creating a new Authenticator
.
The typical Authenticator
is an empty struct implementing the Authenticator
interface:
type MyAuthenticator struct{}
// Ensure you're correctly implementing Authenticator.
var _ auth.Authenticator = (*MyAuthenticator)(nil) // implements Authenticator
The next step is to implement the Authenticate
method. Its purpose is explained at the start of this guide.
In this example, we are going to authenticate the user using a simple token stored in the database.
func (a *MyAuthenticator) Authenticate(request *goyave.Request, user interface{}) error {
token, ok := request.BearerToken()
if !ok {
return fmt.Errorf(lang.Get(request.Lang, "auth.no-credentials-provided"))
}
// Find the struct field tagged with `auth:"token"`
columns := auth.FindColumns(user, "token")
// Find the user in the database using its token
result := database.Conn().Where(columns[0].Name+" = ?", token).First(user)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// User not found, return "Invalid credentials."
return fmt.Errorf(lang.Get(request.Lang, "auth.invalid-credentials"))
}
// Database error
panic(result.Error)
}
// Authentication successful
return nil
}
# auth.Unauthorizer
If you need to override the default behavior when the authentication fails, you can implement the auth.Unauthorizer
interface on your Authenticator
.
func (a *MyAuthenticator) OnUnauthorized(response *goyave.Response, request *goyave.Request, err error) {
response.JSON(http.StatusUnauthorized, map[string]string{"authError": err.Error()})
}
# auth.FindColumns
Find columns in the given struct. A field matches if it has a "auth" tag with the given value.
Returns a slice of found fields, ordered as the input fields
slice.
Promoted fields are matched as well.
If the nth field is not found, the nth value of the returned slice will be nil
.
Parameters | Return |
---|---|
struct interface{} | []*auth.Column |
fields ...string |
Example:
Given the following struct and username
, notatag
, password
:
type TestUser struct {
gorm.Model
Name string `gorm:"type:varchar(100)"`
Password string `gorm:"type:varchar(100)" auth:"password"`
Email string `gorm:"type:varchar(100);uniqueIndex" auth:"username"`
}
fields := auth.FindColumns(user, "username", "notatag", "password")
The result will be the Email
field, nil
and the Password
field.
TIP
The Column
struct is defined as follows:
type Column struct {
Name string
Field *reflect.StructField
}
# Permissions
WARNING
This feature is not implemented yet and is coming in a future release.
Watch (opens new window) the github repository to stay updated.