Authboss renders several different pages during the course of the user's authentication, registration, recovery and management process. The documentation refers to "templates"; more accurately, an Authboss template is the name of a page that it needs to render. It just so happens that HTML templates are the most sensible implementation.
This is where templates are loaded and rendered, and implements the
authboss.Render
interface. The design and flow of control are simple:
-
Pre-load all HTML templates via the
TemplateLoader()
function, which returns aTemplates *
object.TemplateLoader()
walks thecontent
subdirectory to load and parse a master template, fragment templates common to (or frequently used by) page templates and the page templates themselves. TheTemplate
type retains enough state to reload (hot load) all HTML templates when a filesystem change is detected.-
Naming convention:
-
The base name without the
.gohtml
extension is the name by which the page is referenced in theTemplates.templateMap
to retrieve the parsed HTML template. -
Fragment templates in the
content/fragments
subdirectory have leading underscores (e.g.,_login_form
). Vide infra about fragment templates.
-
-
Master layout template (
master_layout.gohtml
)-
This is the wrapper template with the HTML
html
andbody
tags and refers to a template namedcontent
for the actual page content. -
Fragment templates (the
content/fragments
subdirectory) are parsed HTML templates nested within the master layout. See the_login_form
,_logo_splash
and_navbar
template fragments for examples -- these are both nested within the master template and can be referenced by the individual page templates:master_template -> _login_form _logo_splash _navbar
-
-
Individual page templates
-
Load and parse the
.gohtml
files in the top-levelcontent
directory. -
For each parsed template, the master template is cloned and the parsed template nested within, with the name
content
.For example, the
index.gohtml
produces a cloned master template with the following nested content:master_template_clone -> _login_form _logo_splash _navbar content <- the parsed index.gohtml nested template
Templates.templateMap
stores the cloned master template using the naming convention described above:templates.templateMap["index"] = { master_template_clone -> _login_html -> _logo_splash -> _navbar -> content }
-
-
-
Load
(Authboss interface) ensures that a requested Authboss HTML template page is loaded. You can see this in the log:[CONFIG] 2022/08/06 12:59:35 Templates.Load: login present and accounted for. [CONFIG] 2022/08/06 12:59:35 Templates.Load: confirm_html present and accounted for. [CONFIG] 2022/08/06 12:59:35 Templates.Load: confirm_txt present and accounted for. [CONFIG] 2022/08/06 12:59:35 Templates.Load: recover_start present and accounted for. [CONFIG] 2022/08/06 12:59:35 Templates.Load: recover_end present and accounted for. [CONFIG] 2022/08/06 12:59:35 Templates.Load: recover_html present and accounted for. [CONFIG] 2022/08/06 12:59:35 Templates.Load: recover_txt present and accounted for. [CONFIG] 2022/08/06 12:59:35 Templates.Load: register present and accounted for.
If
Load
doesn't find the named page inTemplates.templateMap
, it will log a FATAL message and terminateauthboss-worked
. -
Render
(Authboss interface) renders a named page for Authboss. This just retrieves the pages template fromTemplates.templateMap
and callshtml.ExecuteTemplate
.Note: There are special templates that don't require rendering the master HTML layout. These are used for confirmation and account recovery e-mails, where Authboss crafts a MIME-encoded e-mail. See the
nonRenderedTemplates
map and its usage.
The Render
interface function receives a authboss.HTMLData
object, which is
a map[string]string
. The authboss.HTMLData
object stores associations
between variables referenced in the templates and their values. These
associations are gathered in a HTTP handler function and stored in both the Gin
and the http.Request
contexts with the authboss.CTXKeyData
key.
For example, loggedin
is a boolean flag indicating whether the user is
currently logged in and used as a condition in several templates:
{{ if not .loggedin }}
<!-- show the login form -->
{{ else }}
<!-- stuff if the user is logged in -->
{{ end }}
NOTE: authboss.CTXKeyData
MUST BE USED as the context key if you want
Authboss to pass template variables to your code and HTML templates.
worked_udata.sqlite3
is the SQLite3 database backing store to the
authboss-worked
demo. There are five tables that the demo creates and manages;
the sesssions
table is created and managed by Gin sessions
package.
Table | Purpose |
---|---|
udata | User GUID (primary key), Authboss primary identifier (e-mail) and bcrypt-ed password. |
confirmations | User GUID (primary key, join to udata), confirmation selector, verifier and confirmation status (true/false) |
locked_accounts | User GUID (primary key, join to udata), account lock status (attempts, last attempt time, lock expiration) |
recover_requests | User GUID (primary key, join to udata), recovery selector and verifier, and recovery token expiration |
remember | User GUID (join to udata), "remember me" tokens. Should also have an expiration date/time (not implemented.) |
The the Create()
interface method in abossUData.go
generates a GUID for the
new user, which is the primary key into the other four tables. The GUID
separates the reference to the user from the Authboss Primary Identifer
("PID"), which could be allowed to change. For example, this enables changing
the user's e-mail address at a later time, if you use the user's e-mail as their
primary identifier.
Request handler functions receive a context parameter, which encapsulates useful
information related to the HTTP request, such as form data, query parameters,
HTTP header data, etc. Gin departs from the http
package by providing its own
gin.Context
, whereas other HTTP frameworks use the standard context.Context
.
Authboss expects to retrieve data from the HTTP request's context.Context
and
doesn't know anything about Gin's gin.Context
. This means that if you update
or change data, you have to reinject it into the http.Request
context:
func(ctx *gin.Context) {
// Gin handler function: Stuff goes on, collects more template variables...
// Update the http.Request's context
ctx.Request = r.Clone(context.WithValue(r.Context(), authboss.CTXKeyData, abossCTXData))
// Make the data available to downstream Gin handlers as well:
ctx.Set(string(authboss.CTXKeyData), abossCTXData)
}
- YAML configuration reader:
GetWorkedConfig
- YAML configuration writer:
GenerateWorkedConfig
- The
ConfigData
structure contains all of the demo's configuration data, which is more than the YAML configuration. The YAML portion is embedded withinConfigData
.
- Go structure types to GORM database mappings
- A couple of structures implement the
gorm.TableName()
interface to change the database table name for aesthetic reasons.
-
AuthStorer
contains the GORM connection. -
WorkedUser
contains all of the state related to a user and user management: an embeddedAuthStorer
pointer and an embeddedUserState
structure.- The
AuthStorer
pointer enables the Authboss user-related interface functions (confirmations, lock status, ...) to query the database and avoids carrying that data in theUserState
structure.
- The
-
AuthStorer
specializes theServerStorer
andCreatingServer
interfaces. whereasWorkedUser
specializes theConfirmingServerStorer
,RecoveringServerStorer
,RememberingServerStorer
interfaces.-
ServerStorer
andCreatingServer
return anauthboss.User
→WorkedUser
. -
The other interfaces are get/put interfaces that operate directly on
WorkedUser
or structures related toUserState
.
-
-
The
ConfirmingServerStorer
,RecoveringServerStorer
,RememberingServerStorer
are intended to operate on structure members. The demo sends that data directly to the database as UPSERTs, which puts additional pressure on the underlying database server (SQLite3). There is a pattern to the sequence by which Authboss gets and puts data for these interfaces. However, it would be a suboptimal design to rely on those patterns. (Note: This is not a criticism of Authboss' design.) -
The
WorkedUser
specializations show to use a GORM subquery to join back to theudata
table (UserData
type) to get the user's GUID from their Authboss Primary Identifier (PID).-
An example subquery used in the
ConfirmingServerStorer
(subq
is the subquery):subq := user.AuthStorer.UserDB.Model(&UserData{}).Select("guid").Where(&UserData{Email: user.GetPID()}) result := user.AuthStorer.UserDB.Model(&Confirmations{}). Select("Confirmed"). Where("GUID IN (?)", subq). First(&confirmed)
-
The alternative to using a subquery is an Association join.
-
The demo doesn't generally use association joins, although the code is there to show how it is done. Association joins will fetch the entirety of the
UserData
structure from theudata
table, which puts pressure on the SQLite database. -
Association joins are used when the
WorkedUser
data is acutally needed, such as when retrieving the user via a confirmation or recovery selector (seeLoadByConfirmSelector
andLoadByRecoverSelector
.) -
Some enterprising developer will probably propose the following, "Why not avoid the subquery and just use the
WorkedUser
GUID directly?" Yes, that's a valid optimization, but doesn't check the constraint that the user's primary identifier exists in theudata
table. The subquery is there for sanity checking. You can't be too paranoid when it comes to ensuring data integrity (while keeping that integrity overhead to a minimum.)
-
-
GORM has an implementation concept called
gorm.Model
, which is normally embedded in structures.-
gorm.Model
is not strictly required, and the demo only uses a subset of thegorm.Model
's members. -
Specifically, the demo does not use the
DeletedAt
member:DeletedAt
implements a "soft delete" feature. "Soft delete" doesn't actually delete data; all queries have an extra condition that selects "active" rows whereDeletedAt
is NOT NULL.
-
-
This is where Authboss configuration happens:
configureAuthboss
→(ab *authboss.Authboss, err error)
-
Authboss configuration has two phases:
default.SetCore()
andHTTPBodyReader
.-
Initialize
authboss.Config
members before invokingdefault.SetCore()
. Technically speaking, all you really need to do is set the renderersCore.ViewRenderer
andCore.MailRenderer
. However, ifdefault.SetCore()
ever changes and requires moreauthboss.Config
initialization, the changes won't break your code (much.) -
HTTPBodyReader
is the default implementation that reads JSON or form data and does input validation. The forms have requirements that are lightly documented, if documented at all.-
It looks like the form field names are flexible, but from reading the code, that's well, maybe…
-
default.NewHTTPBodyReader()
takes two boolean parameters: (readJSON, useUsernameNotEmail bool)`.-
readJSON
←false
to read HTML form data,true
if reading JSON. -
useUsernameNotEmail
← false if using e-mail addreses as the user's Primary Identifier (PID) ortrue
if you have a user naming convention.
-
-
Form requirements (refer to the HTML templates for where they are used, when they are used):
Form field Purpose name The user name, if useUsernameNotEmail
is true. If not using user names, don't include this field in your form.email The user's e-mail. The form still requires this field even when useUsernameNotEmail
is true, for confirmation and account recoverypassword The user's password. confirm_password Password confirmation during user registration. This must match the password field. rm "Remember Me" checkbox's value. This is hard-coded and will drive you nuts until you read the Authboss code! -
Data validation: There are six (6) form data validation rulesets (
map[string][]Rules
), and two defaultauthboss.Rule{}
-s,pidRule
andpasswordRule
. Both rules specify requirements for certain form data, such as "must be present", minimum/maximum length, regular expression pattern matching.Page name Rules Explanation login pidRule Ensures that the primary identifier is present in the form (user name or e-mail) register pidRule, passwordRule Validates primary identifier and password format for user registration confirm --- Ensures that a field named "cnf" exists during the confirmation process (hidden form field?) recover_start pidRule Validates the primary identifier's format to recover a user's account recover_end passwordRule Validates the password's format when changing a user's password at the end of account recovery. twofactor_verify_end --- Ensures that a field named "token" exists at the end of the 2FA process (hidden form field?) authboss.Rule{}
has a field name member, which identifies the data to be validated. And that data does get validated.
-
If you want to use different form field names or have more complicated data validation rulesets or some other requirement that
HTTPBodyReader
doesn't address, you will have to roll your own version ofHTTPBodyReader
and implement theRead
interface, as well as sundry other interfaces invoked during data validation. -
UTSL1 even if you don't need to customize
HTTPBodyReader
.HTTPBodyReader
most likely covers 80-90% of the authentication use cases, but it's still instructive to walk through the code. -
The demo relaxes the password requirements from the default. (UTSL for the default password requirements.)
-
-
-
This file has three parts: the Gin router (engine) configuration, the session store and the cookie store.
-
Authboss and Gorilla CSRF middleware functions have to be "wrapped" so that they conform to the Gin handler function interface.
-
The "standard" Gin wrapper package does not expose the inner
swappedResponseWriter
structure. Authboss requires anUnderlyingResponseWriter()
interface implementation to acquire a HTTP response writer object. Pretty tricky to do whenswappedResponseWriter
isn't exported. -
To work around this problem,
abossworked.adapter
is a local copy of the standard Gin wrapper package with anUnderlyingResponseWriter()
interface implementation.
-
-
Gin router (engine) configuration
-
Decode the session, cookie and CSRF seeds from their Base64 encoding.
-
Create the session and cookie stores.
-
Create the
authboss.Authboss
authentication object (vide supra) -
Configure the middleware stack at the web server's root ("/"):
-
Mandatory, in this order. These really do need to come before other middleware executes.
- Gorilla CSRF
- Gin session cookie management
authboss.LoadClientStateMiddleware
-
Conditionally add the Authboss "Remember me" middleware module.
-
Template variable data collection
-
-
Route "/auth" into Authboss. Note that the "/auth" prefix has to be stripped or Authboss won't recognize its own routes (see
authboss.Config.Paths.Mount
). This follows the original sample code's methdology. -
Configure the "/app" route, which is the part of the web server's name space we want protected
-
authboss.Middleware2()
is the function that injects all of the Authboss middleware needed to protect a portion of the web server's name space. -
If enabled in the configuration, only allow confirmed users and users with unlocked accounts to access the "/app" name space.
-
The
app_index
HTML template renders the "/app/" index page.
-
-
Add handlers for additional pages and static content (images).
-
Return the resulting
gin.Engine
object pointer, go off and do good things.
-
-
The session store retrieves the user's session data from the
http.Request
's context and packages it in aSessionState
structure, from which Authboss reads session data.-
This is the driving reason why session middleware handlers have to execute before
authboss.LoadClientStateMiddleware
. We need an updated session store in order to extract up-to-date data. -
The
WriteState
interface method is where Authboss makes updates to session state.WriteState
doesn't write the session cookie into thehttp.ResponseWriter
; we let the GORM session store manage that for us.
-
-
Cookie manager
The cookie manager reads and writes Authboss cookies that should be included
with every request (alternatively, should persist acrosss requests.) The only
cookie that Authboss needs to persist across requests is the "Remember me"
cookie, rm
.
Application and session cookies are handled separately.
Footnotes
-
See the Jargon File entry ↩