Documentation ¶
Overview ¶
Package auth provides boring user authentication code for golang.
Because I'm tired of writing the same things over and over again.
Provides a complete user authentication system, including:
1. Email / password
2. Facebook and Google authentication
3. Change of password / email
4. Forgotten passwords
5. SAML SSO
6. Rate limiting
Tested with SQLITE and Postgresql. To use it, create a database using the sqlx module, and then create an auth.UserDB from that, and then call auth.New() to create an HTTP handler for "/user/" (note the trailing slash). See the example below. It provides the following endpoints which work with GET and POST. It also allows CORS and OPTIONS requests.
If you want, you can write all your own database code by implementing the UserDB interface.
All HTTP responses might have the additional "Status" header which is a user-readable explanation of what went wrong.
Auth ¶
/user/auth has three cases. In case one, pass "email" and "password" and you will receive either an HTTP error, or the UserInfo structure.
In the second case, use "method" and "token" to perform oauth authentication. This will either sign in or create a new user. If the method is "facebook" then the token is used to get the user's email from facebook's servers.
If, when you pass email and password, you get HTTP error 407, that means SSO authentication is required. You should reload the whole web page to the url /user/auth?sso=1&email= giving the email address. This will start the SSO authentication process, and when done it will return to /.
Create ¶
/user/create will create a password user, using the "email" and "password". The user will be signed in and the response will be identical to /user/authenticate or /user/get.
The user is automatically signed in, unless the optional "signin" parameter is "0".
Get ¶
/user/get will retrieve the user's information and return it as JSON, or return code 401 if not signed in.
Signout ¶
/user/signout will forget the user's session cookie. It always returns code 200
Update ¶
/user/update takes two parameters, "email" and "password". If email is non-blank, it changes the user's email. If password is non-blank, it changes the password.
Oauth add ¶
/user/oauth/add performs takes three parameters, "method", "token" and "update_email". It performs oauth authentication and adds the authentication to the user's account so they can later sign in. If "update_email" is true, it also changes the user's email address to the one provided by the oauth provider.
The method parameter can be "facebook" or "google".
Oauth remove ¶
/user/oauth/remove removes the oauth method from the user's account. The only parameter is "method" which can be "facebook" or "google"
Forgot password ¶
/user/forgotpassword just takes an "email" parameter. If the user exists in the system, it sends an email with the password reset token to the user's email address. Otherwise it returns a sensible error message in the Status header.
The text of the email sent is specified in the settings. It must have ${TOKEN} in it. This will be replaced with the actual secret token. See the example.
Reset password ¶
/user/resetpassword takes the "token" parameter and "password". It will update the user's password and also sign them in, returning UserInfo.
SAML ¶
The SAML service provider metadata is accessed from /user/saml/metadata. The SAML ACS url is /user/saml/acs. To use SAML, you will need to override the GetSamlIdentityProviderForUser method and return the identity provider XML metadata based on the user's email address. You will also need to add the XML metadata for the provider using the AddSamlIdentityProviderMetadata method.
Database tables ¶
Auth will automatically create database tables if they do not exist to hold users, sessions, oauth data, and password reset tokens. Passwords are stored as salted values returned by bcrypt. You can see the schema in schema.go
Example ¶
package main import ( "log" "net/http" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" "github.com/smhanov/auth" ) const gmailUser = "support@awesomepeaches.com" const gmailPassword = "awernmx32hdkssk2mssxx" // app password from google func main() { // configure how to send password reset emails settings := auth.DefaultSettings settings.SMTPServer = "smtp.gmail.com:587" settings.SMTPUser = gmailUser settings.SMTPPassword = gmailPassword settings.ForgotPasswordSubject = "Password reset from awesomepeaches.com" settings.ForgotPasswordBody = "Please go to this url to reset your password:\n\n https://awesomepeaches.com/forgot-password/?token=${TOKEN}" settings.EmailFrom = "support@awesomepeaches.com" db, err := sqlx.Open("sqlite3", "mydatabase.db") if err != nil { log.Panic(err) } http.Handle("/user/", auth.New(auth.NewUserDB(db), settings)) log.Fatal(http.ListenAndServe(":8080", nil)) }
Output:
Example (Saml) ¶
Example_saml shows how to register Saml providers and override the GetSamlIdentityProviderForUser method
package main import ( "context" "io" "log" "net/http" "strings" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" "github.com/smhanov/auth" ) // Create our own database, and // override the GetSamlIdentityProviderForUser method type myDB struct{ auth.DB } type myTx struct{ auth.Tx } func (db myDB) Begin(ctx context.Context) auth.Tx { return myTx{db.DB.Begin(ctx)} } func (tx myTx) GetSamlIdentityProviderForUser(email string) string { if email == "user@example.com" { return "" } return tx.GetSamlIdentityProviderByID("https://samltest.id/saml/idp") } func serveMainPage(w http.ResponseWriter, r *http.Request) { w.Write([]byte(mainWebPage)) } // Example_saml shows how to register Saml providers and // override the GetSamlIdentityProviderForUser method func main() { // Open the database rawdb, err := sqlx.Open("sqlite3", "mydatabase.db") if err != nil { log.Panic(err) } db := myDB{auth.NewUserDB(rawdb)} // Download IDP metadata and register it. This only needs to be done once, not // every time the program starts. But for simplicity, we do it here. xml := fetchURL("https://samltest.id/saml/idp") tx := db.Begin(context.Background()) tx.AddSamlIdentityProviderMetadata(auth.GetSamlID(xml), xml) if tx.GetUserByEmail("user@example.com") == 0 { tx.CreatePasswordUser("user@example.com", auth.HashPassword("password")) } tx.Commit() // Register the handler. http.Handle("/user/", auth.New(db, auth.DefaultSettings)) http.HandleFunc("/", serveMainPage) log.Fatal(http.ListenAndServe(":8080", nil)) } // Fetch the URL and return its contents as a string func fetchURL(url string) string { resp, err := http.Get("https://samltest.id/saml/idp") if err != nil { panic(err) } defer resp.Body.Close() buf := new(strings.Builder) io.Copy(buf, resp.Body) return buf.String() } const mainWebPage = `<!DOCTYPE html> <html> <body> <div> To run this, <ol> <li>Download <a href="/user/saml/metadata">the Service Provider Metadata</a> and send it to <a href="https://samltest.id/upload.php">https://samltest.id/upload.php</a> </ol> </div> <div class="wait">Please wait...</div> <div class="not-signed-in"> <h1>You need to sign in.</h1> <div>Please sign in as user@example.com with password "password", or enter any other email to use single sign in.</div> Email: <input type="text" id="email"><br> Password: <input type="text" id="password"><br> <button onclick="signin(false)">Sign in</button> <button onclick="signin(true)">Start Single Sign-On</button> </div> <div class="signed-in"> <h1>Hello, <span class="username"></span></h1> <pre class="info"></pre> <button onclick="signout()">Sign out</button> </div> <script> async function main() { // hide everything until we know if we are signed in. show(".not-signed-in", false); show(".signed-in", false); // get the user information let response = await fetch("/user/get"); show(".wait", false); if (response.status === 401) { // We are not logged in. show(".not-signed-in", true); return; } onsignedin(await response.json()); } async function onsignedin(json) { show(".signed-in", true); show(".not-signed-in", false); document.querySelector(".username").textContent = json.email; document.querySelector(".info").textContent = JSON.stringify(json, null, 4); } async function signin(sso) { let email = document.querySelector("#email").value; let password = document.querySelector("#password").value; if (!sso) { let response = await fetch("/user/auth?email="+encodeURIComponent(email) + "&password="+encodeURIComponent(password)); if (response.status === 200) { onsignedin(await response.json()); return; } else if (response.status !== 407) { alert("Error signing in: " + response.status); } } // for SSO signin, reload the web page location.href = "/user/auth?sso=1&email="+encodeURIComponent(email); } async function signout() { await fetch("/user/signout"); location.reload(); } function show(selector, show) { document.querySelector(selector).style.display = show ? "" : "none"; } main(); </script> </body> </html>`
Output:
Index ¶
- Variables
- func AdvanceTime(amount time.Duration)
- func CORS(fn http.Handler) http.HandlerFunc
- func CheckUserID(tx Tx, r *http.Request) int64
- func CompareHashedPassword(hashedPassword, candidatePassword string) error
- func DoRateLimit(operation string, req *http.Request, user string, rate float64, ...) bool
- func GetIPAddress(request *http.Request) string
- func GetSamlID(xml string) string
- func GetUserID(tx Tx, r *http.Request) int64
- func HasRateLimit(name string) bool
- func HashPassword(password string) string
- func IsRequestSecure(r *http.Request) bool
- func MakeCookie() string
- func New(db DB, settings Settings) http.Handler
- func RateLimitAllows(name string, cost, rate float64, period time.Duration) bool
- func RateLimitCheck(name string, cost, rate float64, period time.Duration) bool
- func RecoverErrors(fn http.Handler) http.HandlerFunc
- func SendError(w http.ResponseWriter, status int, err error)
- func SendJSON(w http.ResponseWriter, thing interface{})
- func VerifyOauth(method, token string) (string, string)
- type DB
- type HTTPError
- type Handler
- type Settings
- type Tx
- type UserDB
- type UserInfo
- type UserTx
- func (tx UserTx) AddOauthUser(method string, foreignID string, userid int64)
- func (tx UserTx) AddSamlIdentityProviderMetadata(id, xml string)
- func (tx UserTx) Commit()
- func (tx UserTx) CreatePasswordResetToken(userid int64, token string, expiry int64)
- func (tx UserTx) CreatePasswordUser(email string, password string) int64
- func (tx UserTx) GetID(cookie string) int64
- func (tx UserTx) GetInfo(userid int64, newAccount bool) UserInfo
- func (tx UserTx) GetOauthMethods(userid int64) []string
- func (tx UserTx) GetOauthUser(method string, foreignID string) int64
- func (tx UserTx) GetPassword(email string) (int64, string)
- func (tx UserTx) GetSamlIdentityProviderByID(id string) string
- func (tx UserTx) GetSamlIdentityProviderForUser(email string) string
- func (tx UserTx) GetUserByEmail(email string) int64
- func (tx UserTx) GetUserByPasswordResetToken(token string) int64
- func (tx UserTx) GetValue(key string) string
- func (tx UserTx) RemoveOauthMethod(userid int64, method string)
- func (tx UserTx) Rollback()
- func (tx UserTx) SetValue(key, value string)
- func (tx UserTx) SignIn(userid int64, cookie string)
- func (tx UserTx) SignOut(userid int64, cookie string)
- func (tx UserTx) UpdateEmail(userid int64, email string)
- func (tx UserTx) UpdatePassword(userid int64, password string)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultSettings = Settings{
ForgotPasswordSubject: `Password reset`,
ForgotPasswordBody: `To reset your password, go to ${URL}`,
}
DefaultSettings provide some reasonable defaults
var ErrorDuplicateUser = errors.New("duplicate user")
ErrorDuplicateUser indicates that a user cannot be created because the email already exists. It should be used instead of the cryptic user database constraint validation error.
ErrorUnauthorized is used when the user is not signed in, but is required to be for the operation.
var TestURL string
TestURL if set, will be used instead of an oauth provider like facebook to make requests.
Functions ¶
func AdvanceTime ¶
AdvanceTime is used during testing to simulate time passing
func CORS ¶
func CORS(fn http.Handler) http.HandlerFunc
CORS wraps an HTTP request handler, adding appropriate cors headers. If CORS is desired, you can wrap the handler with it.
func CheckUserID ¶
CheckUserID returns the userid if the user is signed in, or 0
func CompareHashedPassword ¶
CompareHashedPassword compares the hashed password with the one the user entered (unhashed). It returns no error if the passwords match. The default implementation uses bcrypt.CompareHashAndPassword
func DoRateLimit ¶
func DoRateLimit(operation string, req *http.Request, user string, rate float64, period time.Duration) bool
DoRateLimit will rate limit an operation on both the user and ip address. If it is not allowed, it will return false If it is allowed, it will then assume the operation was carried out and return true
func GetIPAddress ¶
GetRequestIP returns the Ip address of the request, taking into account x-forwarded-for headers.
func GetSamlID ¶
GetSamlID returns the entity ID contained within the XML for the given identity provider.
func GetUserID ¶
GetUserID returns the userid. It panics with an HttpError if the user is not signed in.
func HasRateLimit ¶
HasRateLimit returns true if the bucket for the given operation has any attempts, regardless of wether they have reached the limit or not.
func HashPassword ¶
HashPassword computes the salted, hashed password using bcypt. Panics on error.
func IsRequestSecure ¶
IsRequestSecure returns true if the request used the HTTPS protocol. It also checks for appropriate Forwarding headers.
func MakeCookie ¶
func MakeCookie() string
MakeCookie generates a long random string suitable for use as a session cookie
func RateLimitAllows ¶
RateLimitAllows will return true if the given operation is allowed. Name is an arbitrary string that uniquely identifies the user and operation. cost is the cost of the operation, and rate is the max cost allowed in the given time period.
Example: user 123 logs in, and the maximum attempts allowed are 5 in a 10 minute period.
if auth.RateLimitAllows(name, 1, 5, 10 * time.Minute) { // succeeded }
func RateLimitCheck ¶
RateLimitCheck checks if the given operation would be allowed, but does not update it.
func RecoverErrors ¶
func RecoverErrors(fn http.Handler) http.HandlerFunc
RecoverErrors will wrap an HTTP handler. When a panic occurs, it will print the stack to the log. Secondly, it will return the internal server error with the status header equal to the error string.
func SendError ¶
func SendError(w http.ResponseWriter, status int, err error)
SendError writes an error as a status to the output You don't need to use this but it's handy to have!
func SendJSON ¶
func SendJSON(w http.ResponseWriter, thing interface{})
SendJSON will write a json response and set the appropriate content-type header. You don't need to use this but it's handy to have!
func VerifyOauth ¶
VerifyOauth contacts the oauth provider, specified with method, and retrieves the foriegn user id and foreign email of the user from the token. Returns the foriegn id and email, which can then be used to sign in the user. Valid methods are: "facebook", "google"
Types ¶
type DB ¶
DB is all the operations needed from the database. You can use the built-in userdb provided by this package and override one or more operations.
Any errors should be expressed through panic.
type HTTPError ¶
HTTPError is an error that should be communicated to the user through an http status code.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler is an HTTP Handler that will perform user authentication and management.
type Settings ¶
type Settings struct { // SMTP Server and port SMTPServer string SMTPUser string SMTPPassword string // Eg. "My web site <example@example.com>" EmailFrom string ForgotPasswordSubject string // Forgot password email body. This should have ${TOKEN} in it // which will contain the actual text of the secret token. ForgotPasswordBody string // Alternatively, you can use this to send email SendEmailFn func(email string, url string) // Optionally override password hash from bcrypt default. You may override HashPassword, // or both. If you override HashPassword but not CompareHashedPassword, then // a CompareHashPasswordFn will be created based on HashPasswordFn. HashPasswordFn func(password string) string CompareHashedPasswordFn func(hashedRealPassword, candidatePassword string) error // Context used during initialization DefaultContext context.Context }
Settings is the settings for the auth package
type Tx ¶
type Tx interface { Commit() Rollback() AddOauthUser(method string, foreignid string, userid int64) CreatePasswordUser(email string, password string) int64 CreatePasswordResetToken(userid int64, token string, expiry int64) GetID(cookie string) int64 GetInfo(userid int64, newAccount bool) UserInfo GetOauthMethods(userid int64) []string GetOauthUser(method string, foreignid string) int64 GetPassword(email string) (int64, string) GetUserByEmail(email string) int64 GetUserByPasswordResetToken(token string) int64 RemoveOauthMethod(userid int64, method string) SignIn(userid int64, cookie string) SignOut(userid int64, cookie string) UpdateEmail(userid int64, email string) UpdatePassword(userid int64, password string) // Extra methods added to support SAML GetValue(key string) string SetValue(key, value string) GetSamlIdentityProviderForUser(email string) string GetSamlIdentityProviderByID(id string) string AddSamlIdentityProviderMetadata(id string, xml string) }
Tx is a database transaction that has methods for user authentication. Any error should be communicated by panic()
type UserDB ¶
type UserDB struct {
// contains filtered or unexported fields
}
UserDB is a database that handles user authentication
type UserInfo ¶
type UserInfo interface{}
UserInfo contains whatever information you need about the user for your application. It is returned to the javascript code for successful authentication requests.
func SignInUser ¶
SignInUser performs the final steps of signing in an authenticated user, including creating a session. It returns the info structure that should be sent. You should first commit the transaction and then send this structure, perhaps using the SendJSON helper.
Secure should be set to true if the http request was sent over HTTPs, to restrict usage of the cookie to https only.
Example: info := auth.SignInUser(tx, w, userid, false, auth.IsRequestSecure(r)) tx.Commit() auth.SendJSON(w, info)
type UserTx ¶
UserTx wraps a database transaction
func (UserTx) AddOauthUser ¶
AddOauthUser marks the given OAUTH identify as belonging to this user.
func (UserTx) AddSamlIdentityProviderMetadata ¶
AddSamlIdentityProviderMetadata adds the meta data for the given identity provider to the database. The id should be the one returned by GetSamlID(xml)
func (UserTx) CreatePasswordResetToken ¶
CreatePasswordResetToken creates the password reset token with the given expiry date in seconds
func (UserTx) CreatePasswordUser ¶
CreatePasswordUser creates a user with the given email and password The email is already in lower case and the password is already hashed.
func (UserTx) GetID ¶
GetID returns the userid associated with the cookie value, or 0 if no user is signed in with that cookie.
func (UserTx) GetInfo ¶
GetInfo by default returns a structure containing the user's userid, email, and settings.
func (UserTx) GetOauthMethods ¶
GetOauthMethods returns the oauth methods associated with the given user
func (UserTx) GetOauthUser ¶
GetOauthUser returns the userid assocaited with the given foreign identity, or 0 if none exists.
func (UserTx) GetPassword ¶
GetPassword searches for the salted hashed password for the given email address. The email is assumed to be already in all lower case. It also returns the userid. If not found, userid will be 0
func (UserTx) GetSamlIdentityProviderByID ¶
GetSamlIdentityProviderByID will return the XML Metadata file for the given identity provider, which has previously been added with AddSamlIdentityProviderMetadata
func (UserTx) GetSamlIdentityProviderForUser ¶
GetSamlIdentityProviderForUser returns the SAML provider metadata for a given user. The choice of which provider to use for the email address is entirely contained in this method. You will have to override the DB interface to implement this in your app, maybe distinguishing based on their email domain. If this method returns the empty string, normal authentication is done. Otherwise, the browser is redirected to the identity provider's sign in page.
func (UserTx) GetUserByEmail ¶
GetUserByEmail finds the userid associated with the email, or returns 0 if none exists.
func (UserTx) GetUserByPasswordResetToken ¶
GetUserByPasswordResetToken finds the given userid from the token if not expired. If not found, return 0. If found, then remove all tokens from that user.
func (UserTx) GetValue ¶
GetValue should look up the given value. If not present return the empty string.
func (UserTx) RemoveOauthMethod ¶
RemoveOauthMethod removes the given method from the user's account
func (UserTx) UpdateEmail ¶
UpdateEmail changes the given user's email. Email must be in lower case already.
func (UserTx) UpdatePassword ¶
UpdatePassword changes the given user's password. Password must be already hashed.