Get started with Authboss in Golang

What this

In this post I tell you how to get the /register page of Authboss part way up and running. Yes, part way. Not the full way. I haven't even figured out how to set up all the requirements for this registration module yet, let alone the database.

We'll get to build the binary and go to the /register page. But we won't be able to actually register.

(Please tell me if you've got a complete tutorial out or if you can teach me step by step how to use Authboss properly. Also tell me if you want to add sequels to this piece.)

Who this

I'm a noob and new to Authboss so I might not be able to explain its abstractions all that clearly. And I might get some things wrong. Mail me if I do, please!

Why this

Skip this section if you don't want to read my pre-ramble. Unimportant section here! Move along now, nothing to see here.

Basically, I googled kind of hard for tutorials and guides and couldn't find any. I hear tell of a MySQL implementation somewhere but have seen neither hide nor hair of it.

I thought it would be relatively painless because it's supposed to be plug-and-play. But it really isn't!

I might not end up using it because it's taking me so damn long to get started but I thought, I've already spent a full day digging into its internals and hunted down some of its logic across several of its packages so I guess I should write something about it for other noobs like me.

Also, I think Python is so super popular now due in great part to how comprehensively a lot of its frameworks, libraries, and packages are documented. It's so easy to pick up Python stuff. Golang and its ecosystem are younger, and documentation isn't as comprehensive. This contributes to the much steeper learning curve.

I want Golang to become super popular too. That's why this.

When this

All information and links as correct as far as I know as of 3 June 2019.

How this

I think people usually start by reading the Authboss GitHub page first. Then we go on to check out the sample project and try to run it (doesn't run).

Part of the problem at this point is that the sample project is a full-blown implementation. That means it's none too clear where one thing begins or ends. It brings up a lot of questions. How do you set up the database? How do you hook up http handlers to the stuff we came for, like registration, login, logout, forget password, etc? Are those even provided somewhere?

To get a feel for the code, I decided to focus on a single thing: registration.

You might want to open up but the GitHub page and the sample project as I'll refer to both as we go along.

Starter code

An easy way in is to grab the starter code from the Getting Started section of the GitHub page and putting it into a standard main.go file.

Do note that the starter code uses the Chi router. I've imported it below but not changed the original Chi code for the router yet.

package main

import (
"net/http"

"github.com/gorilla/mux"
"github.com/volatiletech/authboss"
abrenderer "github.com/volatiletech/authboss-renderer"
"github.com/volatiletech/authboss/defaults"
_ "github.com/volatiletech/authboss/register"
)

func main() {
ab := authboss.New()

ab.Config.Storage.Server = myDatabaseImplementation
ab.Config.Storage.SessionState = mySessionImplementation // Delete this.
ab.Config.Storage.CookieState = myCookieImplementation // Delete this.

ab.Config.Paths.Mount = "/authboss"
ab.Config.Paths.RootURL = "https://www.example.com/"

// Should be abrenderer.NewHTML().
ab.Config.Core.ViewRenderer = abrenderer.New("/auth")

defaults.SetCore(&ab.Config, false)

if err := ab.Init(); err != nil {
panic(err)
}

mux.Mount("/authboss", http.StripPrefix("/authboss", ab.Config.Core.Router))
}

First, abrenderer.New() should be abrenderer.NewHTML() instead.

Second, the sample code imports "github.com/volatiletech/authboss-renderer" as is without raising any complaints from my IDE. I don't know why yet. But here, it will. So I've named this import abrenderer.

Third, delete the two lines as indicated in my comments in the code above. We don't want them confusing us right now.

Fourth, the code raises a lot of questions. Let's take them one by one. Easy one first.

What's authboss.New()?

The docs say that it "makes a new instance of authboss with a default configuration". Open this link for a peek at the default configuration. As you might already see, we'll overwrite one or two of them.

How do we change the router?

I don't want to use Chi. I want to use Gorilla mux. So I'll tell you how to change it. We've already imported Gorilla mux. Now, replace this line:

mux.Mount("/authboss", http.StripPrefix("/authboss", ab.Config.Core.Router))

With these lines:

router := mux.NewRouter()
router.PathPrefix("/authboss").Handler(http.StripPrefix("/authboss", ab.Config.Core.Router))

I haven't the faintest clue how Gorilla mux's PathPrefix() works. This might help you understand it if you want to but it's not our focus right now. Suffice to say for now that it manages the /authboss route.

What about other routers? I don't know. I like Gorilla mux. For now.

What are ab.Config.Paths.Mount and ab.Config.Paths.RootURL?

You've seen the default configuration from the link above. Now, check it out in the docs. The Config struct is one big struct. Check out the Paths struct inside. As you can see, .Mount is where all your Authboss routes begin. Here, it's set to /authboss. This means that if your website is example.com, your Authboss registration page would be at example.com/authboss/registration. For us now, it's going to be localhost:8000/auth/registration. Let's override the original config now by changing the code to: ab.Config.Paths.Mount = "/auth".

I tried changing it to just "/" and "". Didn't work. Maybe there's a way to do that but I don't know how yet.

.RootURL is, as the docs say, your "scheme+host+port". In our case, it'll be https:// + localhost + :8000. Set it now: ab.Config.Paths.RootURL = "https://localhost:8000".

What's ab.Config.Core.Router?

If you scroll down down down the Config struct in the docs, you'll find the Core struct. It contains the Router. It basically says the Router manages all Authboss routes. If you click through, you find the Router interface here and it looks like this:

type Router interface {
    http.Handler

    Get(path string, handler http.Handler)
    Post(path string, handler http.Handler)
    Delete(path string, handler http.Handler)
}

Golang interfaces were somewhat hard for me coming from Python. But here's a pretty good entry point to understanding them. Without dissecting it too much here, what the Router interface essentially does is allow each route to have its own Get, Post, and Delete methods. In our case, we'll see these with the /registration route.

Remember that we just added router.PathPrefix("/auth").Handler(http.StripPrefix("/auth", ab.Config.Core.Router))? Note that the route here, and in ab.Config.Paths.Mount = "/auth" must match. The latter merely overrides Authboss' default config. It doesn't serve us the route (I guess). The former is Gorilla mux's router, which actually serves it to us.

Short interruption here

Before we go on with the questions, let's complete our basic setup. Add the server with these lines at the end of our code:

log.Println("http server started") // Optional.
log.Fatal(http.ListenAndServe(":8000", router))

The complete code should look like this:

package main

import (
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/volatiletech/authboss"
abrenderer "github.com/volatiletech/authboss-renderer"
"github.com/volatiletech/authboss/defaults"
_ "github.com/volatiletech/authboss/register"
)

func main() {
ab := authboss.New()

ab.Config.Storage.Server = myDatabaseImplementation

ab.Config.Paths.Mount = "/auth"
ab.Config.Paths.RootURL = "http://localhost:8000"

ab.Config.Core.ViewRenderer = abrenderer.NewHTML("/auth")

defaults.SetCore(&ab.Config, false)

if err := ab.Init(); err != nil {
panic(err)
}

router := mux.NewRouter()
router.PathPrefix("/auth").Handler(http.StripPrefix("/auth", ab.Config.Core.Router))

log.Println("http server started")
log.Fatal(http.ListenAndServe(":8000", router))
}

Let's continue.

Now's a good time to say that this is roughly half the minimum code we need.

Where do we register Authboss modules?

Registering a module in Authboss is said to be as simple as importing it. We've already done that:

import (
    ...
    _ "github.com/volatiletech/authboss/register"
)

The next part is adding the route. Remember we added this above:

router.PathPrefix("/auth").Handler(http.StripPrefix("/auth", ab.Config.Core.Router))

It's set to /auth, but we want /register too. Add this right below it:

router.PathPrefix("/register").Handler(http.StripPrefix("/register", ab.Config.Core.Router))

Whatever module has its own requirements. That is, it may need a whole set of functions, structs, something or other. In our case, the Register module requires a large chunk of code that forms the other half of the minimum we need. We'll go through them next.

What're ab.Config.Core.ViewRenderer and abrenderer.NewHTML()?

If you skim through the same Core struct in the docs, you'll find that .ViewRenderer is basically an interface that helps load Authboss' templates.

authboss-renderer's NewHTML() is the actual code that does it. I don't know exactly how it works but here's the actual code. I suppose you can swap in a different renderer if you like. I don't know how renderers work so I can't advise you on this one.

At this point, we must recognise that NewHTML() takes two arguments. First the route, and then the template directory. We want the /registration route so let's edit it to: abrenderer.NewHTML("/register", "your_template_dir").

If you do make your own templates, make sure they're named the same as the routes. So, the /register route should have a template called register.tpl, for example. This will allow .NewHTML() to find it. If you give .NewHTML() the wrong path to your template, or if you don't supply a directory at all, it will default to authboss-renderer's own templates. In our case, it's this one.

Let's use the default templates. So, delete that fictional template directoryabrenderer.NewHTML("/register", "").

What's ab.Config.Storage.Server and how do we use it?

If we go back to the big Config struct in the docs, we'll find the Storage struct inside it. It seems apparent that the Storage struct helps to manage the storing of different things, like databases, sessions, and cookies.

That's as much as I can tell you. The docs don't describe it much further. If you click through to the Server field, you find our here that it's an interface:

type ServerStorer interface {
    // Load will look up the user based on the passed the PrimaryID
    Load(ctx context.Context, key string) (User, error)

    // Save persists the user in the database, this should never
    // create a user and instead return ErrUserNotFound if the user
    // does not exist.
    Save(ctx context.Context, user User) error
}

But what exactly does it do? How do we hook a database up to it? What does it mean to, as it says here, "look up a user" and to "persists the user"? It's a complete mystery to me.

And, it gets doubly more complicated. When we go back to the requirements for user registration on the main Authboss GitHub page, it says that a CreatingServerStorer is needed as a ServerStorer.

Huh??? Your guess is as good as mine.

After a day of pain, though, I figured out that Authboss' Register module requires that we create every method listed in the ServerStorer and CreatingServerStorer interfaces. That means Load() and Save() as seen above and New() and Create() as indicated here:

type CreatingServerStorer interface {
    ServerStorer

    // New creates a blank user, it is not yet persisted in the database
    // but is just for storing data
    New(ctx context.Context) User
    // Create the user in storage, it should not overwrite a user
    // and should return ErrUserFound if it currently exists.
    Create(ctx context.Context, user User) error
}

If we do not provide these methods, we can't build the project. Golang will complain a lot.

I will steal from the Authboss sample project to show the minimum we need to do to at least build the binary and get to the /register page.

Getting started with the storer

Authboss' sample project passes a thing called NewMemStorer() to ab.Config.Storage.Server. What is it? I don't know but it looks like this:

// MemStorer stores users in memory
type MemStorer struct {
Users map[string]User
Tokens map[string][]string
}

// NewMemStorer constructor
func NewMemStorer() *MemStorer {
return &MemStorer{
Users: map[string]User{
"rick@councilofricks.com": User{
  ID: 1,
Name: "Rick",
Password: "$2a$10$XtW/BrS5HeYIuOCXYe8DFuInetDMdaarMUJEOg/VA/JAIDgw3l4aG", // pass = 1234
Email: "rick@councilofricks.com",
Confirmed: true,
SMSSeedPhoneNumber: "(777)-123-4567",
},
},
Tokens: make(map[string][]string),
}
}

It's a method for a MemStorer struct. And it's got a hard-coded user in it. I suppose this means we're supposed to implement in this function a way to add a user who registers?

But then, what's with the New() and Create() functions? And how do we hook these up as handlers to the /register route? No idea!

No matter. We're brave souls. We follow through. We use it: ab.Config.Storage.Server = NewMemStorer().

Paste the MemStorer struct and the NewMemStorer method into your main.go as well. They're needed. You can place them above or below main(), it doesn't matter for now.

If you try to build the binary or run main.go now, Golang will complain that the methods listed in the ServerStorer and CreatingServerStorer structs are missing. Let's copy them over from the sample project:

// Save the user
func (m MemStorer) Save(ctx context.Context, user authboss.User) error {
u := user.(*User)
m.Users[u.Email] = *u

return nil
}

// Load the user
func (m MemStorer) Load(ctx context.Context, key string) (user authboss.User, err error) {
// Check to see if our key is actually an oauth2 pid
provider, uid, err := authboss.ParseOAuth2PID(key)
if err == nil {
for _, u := range m.Users {
if u.OAuth2Provider == provider && u.OAuth2UID == uid {
// debugln("Loaded OAuth2 user:", u.Email)
return &u, nil
}
}

return nil, authboss.ErrUserNotFound
}

u, ok := m.Users[key]
if !ok {
return nil, authboss.ErrUserNotFound
}

// debugln("Loaded user:", u.Name)
return &u, nil
}

// New user creation
func (m MemStorer) New(ctx context.Context) authboss.User {
return &User{}
}

// Create the user
func (m MemStorer) Create(ctx context.Context, user authboss.User) error {
// fmt.Println("create here")
u := user.(*User)
// fmt.Println(u)
if _, ok := m.Users[u.Email]; ok {
return authboss.ErrUserFound
}

// debugln("Created new user:", u.Name)
m.Users[u.Email] = *u
return nil
}

We probably didn't need the User bit right now but I've added them to give an idea of what still needs to be done after this whole charades we've been through. I've commented out the debug bits because we don't need them now.

Oh, we need User too

Because we've added the User, we now need also the User struct and, necessarily, its two methods:

// User struct for authboss
type User struct {
ID int

// Non-authboss related field
Name string

// Auth
Email string `json:"email"`
Password string

// Confirm
ConfirmSelector string
ConfirmVerifier string
Confirmed bool

// Lock
AttemptCount int
LastAttempt time.Time
Locked time.Time

// Recover
RecoverSelector string
RecoverVerifier string
RecoverTokenExpiry time.Time

// OAuth2
OAuth2UID string
OAuth2Provider string
OAuth2AccessToken string
OAuth2RefreshToken string
OAuth2Expiry time.Time

// 2fa
TOTPSecretKey string
SMSPhoneNumber string
SMSSeedPhoneNumber string
RecoveryCodes string

// Remember is in another table
}

// PutPID into user
func (u *User) PutPID(pid string) { u.Email = pid }

// GetPID from user
func (u User) GetPID() string { return u.Email }

The full code

In full, our code should look like this now:

package main

import (
"context"
"fmt"
"log"
"net/http"
"time"

"github.com/gorilla/mux"
"github.com/volatiletech/authboss"
abrenderer "github.com/volatiletech/authboss-renderer"
"github.com/volatiletech/authboss/defaults"
_ "github.com/volatiletech/authboss/register"
)

func main() {
ab := authboss.New()

ab.Config.Storage.Server = NewMemStorer()

ab.Config.Paths.Mount = "/auth"
ab.Config.Paths.RootURL = "http://localhost:8000"

ab.Config.Core.ViewRenderer = abrenderer.NewHTML("/register", "")

// Set up defaults for basically everything besides the ViewRenderer/MailRenderer in the HTTP stack
defaults.SetCore(&ab.Config, false, false)

if err := ab.Init(); err != nil {
panic(err)
}

router := mux.NewRouter()
router.PathPrefix("/auth").Handler(http.StripPrefix("/auth", ab.Config.Core.Router))
router.PathPrefix("/register").Handler(http.StripPrefix("/register", ab.Config.Core.Router))

log.Println("http server started")
log.Fatal(http.ListenAndServe(":8000", router))
}

// User struct for authboss
type User struct {
ID int

// Non-authboss related field
Name string

// Auth
Email string `json:"email"`
Password string

// Confirm
ConfirmSelector string
ConfirmVerifier string
Confirmed bool

// Lock
AttemptCount int
LastAttempt time.Time
Locked time.Time

// Recover
RecoverSelector string
RecoverVerifier string
RecoverTokenExpiry time.Time

// OAuth2
OAuth2UID string
OAuth2Provider string
OAuth2AccessToken string
OAuth2RefreshToken string
OAuth2Expiry time.Time

// 2fa
TOTPSecretKey string
SMSPhoneNumber string
SMSSeedPhoneNumber string
RecoveryCodes string

// Remember is in another table
}

// PutPID into user
func (u *User) PutPID(pid string) { u.Email = pid }

// GetPID from user
func (u User) GetPID() string { return u.Email }

// MemStorer stores users in memory
type MemStorer struct {
Users map[string]User
Tokens map[string][]string
}

// NewMemStorer constructor
func NewMemStorer() *MemStorer {
// fmt.Println("NewMemStorer?")
return &MemStorer{
Users: map[string]User{
"rick@councilofricks.com": User{
ID: 1,
Name: "Rick",
Password: "123pw", // pass = 1234
Email: "rick@councilofricks.com",
Confirmed: true,
SMSSeedPhoneNumber: "(777)-123-4567",
},
},
Tokens: make(map[string][]string)}
}

// Save the user
func (m MemStorer) Save(ctx context.Context, user authboss.User) error {
u := user.(*User)
m.Users[u.Email] = *u

return nil
}

// Load the user
func (m MemStorer) Load(ctx context.Context, key string) (user authboss.User, err error) {
// Check to see if our key is actually an oauth2 pid
provider, uid, err := authboss.ParseOAuth2PID(key)
if err == nil {
for _, u := range m.Users {
if u.OAuth2Provider == provider && u.OAuth2UID == uid {
// debugln("Loaded OAuth2 user:", u.Email)
return &u, nil
}
}

return nil, authboss.ErrUserNotFound
}

u, ok := m.Users[key]
if !ok {
return nil, authboss.ErrUserNotFound
}

// debugln("Loaded user:", u.Name)
return &u, nil
}

// New user creation
func (m MemStorer) New(ctx context.Context) authboss.User {
return &User{}
}

// Create the user
func (m MemStorer) Create(ctx context.Context, user authboss.User) error {
// fmt.Println("create here")
u := user.(*User)
// fmt.Println(u)
if _, ok := m.Users[u.Email]; ok {
return authboss.ErrUserFound
}

// debugln("Created new user:", u.Name)
m.Users[u.Email] = *u
return nil
}

Run the thing

You may now either run it with go run main.go or build the binary and run it immediately after with go build main.go && ./main. I much prefer the latter.

Go to localhost:8000/auth/register and you should see the registration page. If you try to register, you'll see your terminal say "[INFO]: registration validation failed".

Probably because we haven't set up the database and implemented all the required methods properly. How to do these? Beats me!

On to the final question.

What's defaults.SetCore()?

You might have noticed that I surreptitiously changed defaults.SetCore() in the full code. That's because it actually requires three arguments, not two. So, it's now: defaults.SetCore(&ab.Config, false, false).

You'll find the code here:

// SetCore creates instances of all the default pieces
// with the exception of ViewRenderer which should be already set
// before calling this method.
func SetCore(config *authboss.Config, readJSON, useUsername bool) {
logger := NewLogger(os.Stdout)

config.Core.Router = NewRouter()
config.Core.ErrorHandler = NewErrorHandler(logger)
config.Core.Responder = NewResponder(config.Core.ViewRenderer)
config.Core.Redirector = NewRedirector(config.Core.ViewRenderer, authboss.FormValueRedirect)
config.Core.BodyReader = NewHTTPBodyReader(readJSON, useUsername)
config.Core.Mailer = NewLogMailer(os.Stdout)
config.Core.Logger = logger
}

If you set its readJSON parameter to true, Authboss will complain that there's an error. I don't know why. I don't know anything about this method.

On this note of resignation, I end this piece.

Comments

There are currently no comments

New Comment

required

required (not published)

optional

required