Guides
Local Authentication
In many use-cases, you’ll need to implement user authentication in your apps.
Buffalo had a native support for Auth until version v0.9.4
. Since then, it was moved into it’s own plugin, https://github.com/gobuffalo/buffalo-auth.
Installation
To install the buffalo-auth
plugin, run the following command at your project route:
$ buffalo plugins install github.com/gobuffalo/buffalo-auth
Generator
$ buffalo g auth
create models/user.go
create models/user_test.go
run goimports -w actions/actions_test.go actions/app.go actions/home.go actions/home_test.go actions/render.go grifts/db.go grifts/init.go main.go models/models.go models/models_test.go models/user.go models/user_test.go
create migrations/20180910062057_create_users.up.fizz
create migrations/20180910062057_create_users.down.fizz
create actions/auth.go
create actions/auth_test.go
create actions/users.go
create actions/users_test.go
create models/user_test.go
create actions/home_test.go
create templates/auth/new.html
create templates/index.html
create templates/users/new.html
Example Usage
Actions
// actions/app.go
package actions
import (
"github.com/gobuffalo/buffalo"
"github.com/gobuffalo/buffalo/middleware"
"github.com/gobuffalo/buffalo/middleware/ssl"
"github.com/gobuffalo/envy"
"github.com/unrolled/secure"
"coke/models"
"github.com/gobuffalo/buffalo/middleware/csrf"
"github.com/gobuffalo/buffalo/middleware/i18n"
"github.com/gobuffalo/packr"
)
// ENV is used to help switch settings based on where the
// application is being run. Default is "development".
var ENV = envy.Get("GO_ENV", "development")
var app *buffalo.App
var T *i18n.Translator
// App is where all routes and middleware for buffalo
// should be defined. This is the nerve center of your
// application.
func App() *buffalo.App {
if app == nil {
app = buffalo.New(buffalo.Options{
Env: ENV,
SessionName: "_coke_session",
})
// Automatically redirect to SSL
app.Use(forceSSL())
if ENV == "development" {
app.Use(middleware.ParameterLogger)
}
// Protect against CSRF attacks. https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
// Remove to disable this.
app.Use(csrf.New)
// Wraps each request in a transaction.
// c.Value("tx").(*pop.PopTransaction)
// Remove to disable this.
app.Use(middleware.PopTransaction(models.DB))
// Setup and use translations:
app.Use(translations())
app.GET("/", HomeHandler)
app.Use(SetCurrentUser)
app.Use(Authorize)
app.GET("/users/new", UsersNew)
app.POST("/users", UsersCreate)
app.GET("/signin", AuthNew)
app.POST("/signin", AuthCreate)
app.DELETE("/signout", AuthDestroy)
app.Middleware.Skip(Authorize, HomeHandler, UsersNew, UsersCreate, AuthNew, AuthCreate)
app.ServeFiles("/", assetsBox) // serve files from the public directory
}
return app
}
// translations will load locale files, set up the translator `actions.T`,
// and will return a middleware to use to load the correct locale for each
// request.
// for more information: https://gobuffalo.io/en/docs/localization
func translations() buffalo.MiddlewareFunc {
var err error
if T, err = i18n.New(packr.NewBox("../locales"), "en-US"); err != nil {
app.Stop(err)
}
return T.Middleware()
}
// forceSSL will return a middleware that will redirect an incoming request
// if it is not HTTPS. "http://example.com" => "https://example.com".
// This middleware does **not** enable SSL. for your application. To do that
// we recommend using a proxy: https://gobuffalo.io/en/docs/proxy
// for more information: https://github.com/unrolled/secure/
func forceSSL() buffalo.MiddlewareFunc {
return ssl.ForceSSL(secure.Options{
SSLRedirect: ENV == "production",
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
})
}
// actions/auth.go
package actions
import (
"database/sql"
"strings"
"coke/models"
"github.com/gobuffalo/buffalo"
"github.com/gobuffalo/pop"
"github.com/gobuffalo/validate"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
// AuthNew loads the signin page
func AuthNew(c buffalo.Context) error {
c.Set("user", models.User{})
return c.Render(200, r.HTML("auth/new.html"))
}
// AuthCreate attempts to log the user in with an existing account.
func AuthCreate(c buffalo.Context) error {
u := &models.User{}
if err := c.Bind(u); err != nil {
return errors.WithStack(err)
}
tx := c.Value("tx").(*pop.Connection)
// find a user with the email
err := tx.Where("email = ?", strings.ToLower(u.Email)).First(u)
// helper function to handle bad attempts
bad := func() error {
c.Set("user", u)
verrs := validate.NewErrors()
verrs.Add("email", "invalid email/password")
c.Set("errors", verrs)
return c.Render(422, r.HTML("auth/new.html"))
}
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
// couldn't find an user with the supplied email address.
return bad()
}
return errors.WithStack(err)
}
// confirm that the given password matches the hashed password from the db
err = bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(u.Password))
if err != nil {
return bad()
}
c.Session().Set("current_user_id", u.ID)
c.Flash().Add("success", "Welcome Back to Buffalo!")
return c.Redirect(302, "/")
}
// AuthDestroy clears the session and logs a user out
func AuthDestroy(c buffalo.Context) error {
c.Session().Clear()
c.Flash().Add("success", "You have been logged out!")
return c.Redirect(302, "/")
}
// actions/auth_test.go
package actions
import (
"coke/models"
)
func (as *ActionSuite) Test_Auth_New() {
res := as.HTML("/signin").Get()
as.Equal(200, res.Code)
as.Contains(res.Body.String(), "Sign In")
}
func (as *ActionSuite) Test_Auth_Create() {
u := &models.User{
Email: "mark@example.com",
Password: "password",
PasswordConfirmation: "password",
}
verrs, err := u.Create(as.DB)
as.NoError(err)
as.False(verrs.HasAny())
res := as.HTML("/signin").Post(u)
as.Equal(302, res.Code)
as.Equal("/", res.Location())
}
func (as *ActionSuite) Test_Auth_Create_UnknownUser() {
u := &models.User{
Email: "mark@example.com",
Password: "password",
}
res := as.HTML("/signin").Post(u)
as.Equal(422, res.Code)
as.Contains(res.Body.String(), "invalid email/password")
}
func (as *ActionSuite) Test_Auth_Create_BadPassword() {
u := &models.User{
Email: "mark@example.com",
Password: "password",
PasswordConfirmation: "password",
}
verrs, err := u.Create(as.DB)
as.NoError(err)
as.False(verrs.HasAny())
u.Password = "bad"
res := as.HTML("/signin").Post(u)
as.Equal(422, res.Code)
as.Contains(res.Body.String(), "invalid email/password")
}
// actions/home.go
package actions
import "github.com/gobuffalo/buffalo"
// HomeHandler is a default handler to serve up
// a home page.
func HomeHandler(c buffalo.Context) error {
return c.Render(200, r.HTML("index.html"))
}
// actions/home_test.go
package actions
import "coke/models"
func (as *ActionSuite) Test_HomeHandler() {
res := as.HTML("/").Get()
as.Equal(200, res.Code)
as.Contains(res.Body.String(), "Sign In")
}
func (as *ActionSuite) Test_HomeHandler_LoggedIn() {
u := &models.User{
Email: "mark@example.com",
Password: "password",
PasswordConfirmation: "password",
}
verrs, err := u.Create(as.DB)
as.NoError(err)
as.False(verrs.HasAny())
as.Session.Set("current_user_id", u.ID)
res := as.HTML("/").Get()
as.Equal(200, res.Code)
as.Contains(res.Body.String(), "Sign Out")
as.Session.Clear()
res = as.HTML("/").Get()
as.Equal(200, res.Code)
as.Contains(res.Body.String(), "Sign In")
}
// actions/users.go
package actions
import (
"coke/models"
"github.com/gobuffalo/buffalo"
"github.com/gobuffalo/pop"
"github.com/pkg/errors"
)
func UsersNew(c buffalo.Context) error {
u := models.User{}
c.Set("user", u)
return c.Render(200, r.HTML("users/new.html"))
}
// UsersCreate registers a new user with the application.
func UsersCreate(c buffalo.Context) error {
u := &models.User{}
if err := c.Bind(u); err != nil {
return errors.WithStack(err)
}
tx := c.Value("tx").(*pop.Connection)
verrs, err := u.Create(tx)
if err != nil {
return errors.WithStack(err)
}
if verrs.HasAny() {
c.Set("user", u)
c.Set("errors", verrs)
return c.Render(200, r.HTML("users/new.html"))
}
c.Session().Set("current_user_id", u.ID)
c.Flash().Add("success", "Welcome to Buffalo!")
return c.Redirect(302, "/")
}
// SetCurrentUser attempts to find a user based on the current_user_id
// in the session. If one is found it is set on the context.
func SetCurrentUser(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
if uid := c.Session().Get("current_user_id"); uid != nil {
u := &models.User{}
tx := c.Value("tx").(*pop.Connection)
err := tx.Find(u, uid)
if err != nil {
return errors.WithStack(err)
}
c.Set("current_user", u)
}
return next(c)
}
}
// Authorize require a user be logged in before accessing a route
func Authorize(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
if uid := c.Session().Get("current_user_id"); uid == nil {
c.Flash().Add("danger", "You must be authorized to see that page")
return c.Redirect(302, "/")
}
return next(c)
}
}
// actions/users_test.go
package actions
import (
"coke/models"
)
func (as *ActionSuite) Test_Users_New() {
res := as.HTML("/users/new").Get()
as.Equal(200, res.Code)
}
func (as *ActionSuite) Test_Users_Create() {
count, err := as.DB.Count("users")
as.NoError(err)
as.Equal(0, count)
u := &models.User{
Email: "mark@example.com",
Password: "password",
PasswordConfirmation: "password",
}
res := as.HTML("/users").Post(u)
as.Equal(302, res.Code)
count, err = as.DB.Count("users")
as.NoError(err)
as.Equal(1, count)
}
Models
// models/user.go
package models
import (
"encoding/json"
"time"
"github.com/gobuffalo/pop"
"github.com/gobuffalo/uuid"
"github.com/gobuffalo/validate"
"github.com/gobuffalo/validate/validators"
"strings"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
type User struct {
ID uuid.UUID `json:"id" db:"id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
Email string `json:"email" db:"email"`
PasswordHash string `json:"password_hash" db:"password_hash"`
Password string `json:"-" db:"-"`
PasswordConfirmation string `json:"-" db:"-"`
}
// String is not required by pop and may be deleted
func (u User) String() string {
ju, _ := json.Marshal(u)
return string(ju)
}
// Users is not required by pop and may be deleted
type Users []User
// String is not required by pop and may be deleted
func (u Users) String() string {
ju, _ := json.Marshal(u)
return string(ju)
}
// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
// This method is not required and may be deleted.
func (u *User) Validate(tx *pop.Connection) (*validate.Errors, error) {
var err error
return validate.Validate(
&validators.StringIsPresent{Field: u.Email, Name: "Email"},
&validators.StringIsPresent{Field: u.PasswordHash, Name: "PasswordHash"},
// check to see if the email address is already taken:
&validators.FuncValidator{
Field: u.Email,
Name: "Email",
Message: "%s is already taken",
Fn: func() bool {
var b bool
q := tx.Where("email = ?", u.Email)
if u.ID != uuid.Nil {
q = q.Where("id != ?", u.ID)
}
b, err = q.Exists(u)
if err != nil {
return false
}
return !b
},
},
), err
}
// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method.
// This method is not required and may be deleted.
func (u *User) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) {
var err error
return validate.Validate(
&validators.StringIsPresent{Field: u.Password, Name: "Password"},
&validators.StringsMatch{Name: "Password", Field: u.Password, Field2: u.PasswordConfirmation, Message: "Password does not match confirmation"},
), err
}
// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method.
// This method is not required and may be deleted.
func (u *User) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) {
return validate.NewErrors(), nil
}
// Create wraps up the pattern of encrypting the password and
// running validations. Useful when writing tests.
func (u *User) Create(tx *pop.Connection) (*validate.Errors, error) {
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
ph, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return validate.NewErrors(), errors.WithStack(err)
}
u.PasswordHash = string(ph)
return tx.ValidateAndCreate(u)
}
// models/user_test.go
package models_test
import (
"coke/models"
)
func (ms *ModelSuite) Test_User_Create() {
count, err := ms.DB.Count("users")
ms.NoError(err)
ms.Equal(0, count)
u := &models.User{
Email: "mark@example.com",
Password: "password",
PasswordConfirmation: "password",
}
ms.Zero(u.PasswordHash)
verrs, err := u.Create(ms.DB)
ms.NoError(err)
ms.False(verrs.HasAny())
ms.NotZero(u.PasswordHash)
count, err = ms.DB.Count("users")
ms.NoError(err)
ms.Equal(1, count)
}
func (ms *ModelSuite) Test_User_Create_ValidationErrors() {
count, err := ms.DB.Count("users")
ms.NoError(err)
ms.Equal(0, count)
u := &models.User{
Password: "password",
}
ms.Zero(u.PasswordHash)
verrs, err := u.Create(ms.DB)
ms.NoError(err)
ms.True(verrs.HasAny())
count, err = ms.DB.Count("users")
ms.NoError(err)
ms.Equal(0, count)
}
func (ms *ModelSuite) Test_User_Create_UserExists() {
count, err := ms.DB.Count("users")
ms.NoError(err)
ms.Equal(0, count)
u := &models.User{
Email: "mark@example.com",
Password: "password",
PasswordConfirmation: "password",
}
ms.Zero(u.PasswordHash)
verrs, err := u.Create(ms.DB)
ms.NoError(err)
ms.False(verrs.HasAny())
ms.NotZero(u.PasswordHash)
count, err = ms.DB.Count("users")
ms.NoError(err)
ms.Equal(1, count)
u = &models.User{
Email: "mark@example.com",
Password: "password",
}
verrs, err = u.Create(ms.DB)
ms.NoError(err)
ms.True(verrs.HasAny())
count, err = ms.DB.Count("users")
ms.NoError(err)
ms.Equal(1, count)
}
Migrations
// migrations/20180910062057_create_users.down.fizz
drop_table("users")
// migrations/20180910062057_create_users.up.fizz
create_table("users") {
t.Column("id", "uuid", {"primary": true})
t.Column("email", "string", {})
t.Column("password_hash", "string", {})
}
Templates
// templates/auth/new.html
<style>
.auth-wrapper{
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.auth-wrapper .sign-form{
max-width: 350px;
width: 100%;
padding: 0 20px;
}
.auth-wrapper h1{
margin-bottom: 20px;
}
</style>
<div class="auth-wrapper">
<div class="sign-form">
<h1>Sign In</h1>
<%= form_for(user, {action: signinPath()}) { %>
<%= f.InputTag("Email") %>
<%= f.InputTag("Password", {type: "password"}) %>
<button class="btn btn-success">Sign In!</button>
<% } %>
</div>
</div>
// templates/new/new.html
<style>
.auth-wrapper{
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.auth-wrapper .sign-form{
max-width: 350px;
width: 100%;
padding: 0 20px;
}
.auth-wrapper h1{
margin-bottom: 20px;
}
</style>
<div class="auth-wrapper">
<div class="sign-form">
<h1>register</h1>
<%= form_for(user, {action: userspath()}) { %>
<%= f.inputtag("email") %>
<%= f.inputtag("password", {type: "password"}) %>
<%= f.inputtag("passwordconfirmation", {type: "password"}) %>
<button class="btn btn-success">register!</button>
<% } %>
</div>
</div>
// templates/index.html
<style>
.auth-center{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.sign-in-btn{
margin-right: 10px;
}
</style>
<div class="auth-center">
<%= if (current_user) { %>
<h1><%= current_user.email %></h1>
<a href="/signout" data-method="delete">sign out</a>
<% } else { %>
<a href="/signin" class="btn btn-primary">sign in</a>
<a href="/users/new" class="btn btn-success">register</a>
<% } %>
</div>