1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-02-07 13:41:55 +02:00
This commit is contained in:
Aaron L 2018-07-17 07:09:38 -07:00
parent 807a692e26
commit 48e83e1a2a
8 changed files with 324 additions and 13 deletions

View File

@ -108,8 +108,9 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error {
}
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: a.Authboss.Paths.AuthLoginOK,
Code: http.StatusTemporaryRedirect,
RedirectPath: a.Authboss.Paths.AuthLoginOK,
FollowRedirParam: true,
}
return a.Authboss.Core.Redirector.Redirect(w, r, ro)
}

View File

@ -8,7 +8,10 @@ package authboss
import (
"context"
"fmt"
"net/http"
"net/url"
"path"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
@ -77,19 +80,53 @@ func (a *Authboss) UpdatePassword(ctx context.Context, user AuthableUser, newPas
return rmStorer.DelRememberTokens(ctx, user.GetPID())
}
// Middleware prevents someone from accessing a route by returning a 404 if they are not logged in.
// This middleware also loads the current user.
func Middleware(ab *Authboss) func(http.Handler) http.Handler {
// Middleware prevents someone from accessing a route they are not allowed to.
// It allows the user through if they are logged in.
//
// If redirectToLogin is true, the user will be redirected to the login page, otherwise they will
// get a 404. The redirect goes to: mountPath/login, this means it's expected that the auth module
// is loaded if this is set to true.
//
// If allowHalfAuth is true then half-authed users are allowed through, otherwise a half-authed
// user will not be allowed through.
func Middleware(ab *Authboss, redirectToLogin bool, allowHalfAuth bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := ab.RequestLogger(r)
fail := func(w http.ResponseWriter, r *http.Request) {
if redirectToLogin {
log.Infof("redirecting unauthorized user to login from: %s", r.URL.Path)
vals := make(url.Values)
vals.Set(FormValueRedirect, r.URL.Path)
ro := RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "please re-login",
RedirectPath: path.Join(ab.Config.Paths.Mount, fmt.Sprintf("/login?%s", vals.Encode())),
}
if err := ab.Config.Core.Redirector.Redirect(w, r, ro); err != nil {
log.Errorf("failed to redirect user during authboss.Middleware redirect: %+v", err)
return
}
}
log.Infof("not found for unauthorized user at: %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
if !allowHalfAuth && !IsFullyAuthed(r) {
fail(w, r)
return
}
if u, err := ab.LoadCurrentUser(&r); err != nil {
log.Errorf("error fetching current user: %+v", err)
w.WriteHeader(http.StatusInternalServerError)
return
} else if u == nil {
log.Infof("providing not found for unauthorized user at: %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
fail(w, r)
return
} else {
next.ServeHTTP(w, r)

View File

@ -221,6 +221,13 @@ func (c *ClientStateResponseWriter) putClientState() error {
return nil
}
// IsFullyAuthed returns false if the user has a HalfAuth
// in his session.
func IsFullyAuthed(r *http.Request) bool {
_, hasHalfAuth := GetSession(r, SessionHalfAuthKey)
return !hasHalfAuth
}
// DelKnownSession deletes all known session variables, effectively
// logging a user out.
func DelKnownSession(w http.ResponseWriter) {

View File

@ -11,6 +11,9 @@ type Config struct {
Paths struct {
// Mount is the path to mount authboss's routes at (eg /auth).
Mount string
// NotAuthorized is the default URL to kick users back to when
// they attempt an action that requires them to be logged in and they're not auth'd
NotAuthorized string
// AuthLoginOK is the redirect path after a successful authentication.
AuthLoginOK string
@ -148,6 +151,7 @@ type Config struct {
// Defaults sets the configuration's default values.
func (c *Config) Defaults() {
c.Paths.Mount = "/auth"
c.Paths.NotAuthorized = "/"
c.Paths.AuthLoginOK = "/"
c.Paths.ConfirmOK = "/"
c.Paths.ConfirmNotOK = "/"

View File

@ -6,12 +6,6 @@ import (
"github.com/volatiletech/authboss"
)
const (
// RedirectFormValueName is the name of the form field
// in the http request that will be used when redirecting
RedirectFormValueName = "redir"
)
// Responder helps respond to http requests
type Responder struct {
Renderer authboss.Renderer

232
otp/otp.go Normal file
View File

@ -0,0 +1,232 @@
// Package otp allows authentication through a one time password
// instead of a traditional password.
package otp
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/volatiletech/authboss"
"golang.org/x/crypto/bcrypt"
)
const (
// PageLogin is for identifying the login page for parsing & validation
PageLogin = "loginotp"
// PageAdd is for adding an otp to the user
PageAdd = "addotp"
// PageClear is for deleting all the otps from the user
PageClear = "clearotp"
// DataNumberOTPs shows the number of otps for add/clear operations
DataNumberOTPs = "notps"
// DataNewOTP shows the new otp that was added
DataOTP = "otp"
)
// User for one time passwords
type User interface {
// GetOTPs retrieves a string of comma separated bcrypt'd one time passwords
GetOTPs() string
// PutOTPs puts a string of comma separated bcrypt'd one time passwords
PutOTPs(string)
}
// MustBeOTPable ensures the user can use one time passwords
func MustBeOTPable(user authboss.User) User {
u, ok := user.(User)
if !ok {
panic(fmt.Sprintf("could not upgrade user to an authable user, type: %T", u))
}
return u
}
func init() {
authboss.RegisterModule("otp", &OTP{})
}
// OTP module
type OTP struct {
*authboss.Authboss
}
// Init module
func (o *OTP) Init(ab *authboss.Authboss) (err error) {
o.Authboss = ab
if err = o.Authboss.Config.Core.ViewRenderer.Load(PageLogin, PageAdd, PageClear); err != nil {
return err
}
o.Authboss.Config.Core.Router.Get("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginGet))
o.Authboss.Config.Core.Router.Post("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginPost))
middleware := authboss.Middleware(ab, true, false)
o.Authboss.Config.Core.Router.Get("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddGet)))
o.Authboss.Config.Core.Router.Post("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddPost)))
o.Authboss.Config.Core.Router.Get("/otp/clear", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.ClearGet)))
o.Authboss.Config.Core.Router.Post("/otp/clear", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.ClearPost)))
return nil
}
// LoginGet simply displays the login form
func (o *OTP) LoginGet(w http.ResponseWriter, r *http.Request) error {
return o.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, nil)
}
// LoginPost attempts to validate the credentials passed in
// to log in a user.
func (o *OTP) LoginPost(w http.ResponseWriter, r *http.Request) error {
logger := o.RequestLogger(r)
validatable, err := o.Authboss.Core.BodyReader.Read(PageLogin, r)
if err != nil {
return err
}
// Skip validation since all the validation happens during the database lookup and
// password check.
creds := authboss.MustHaveUserValues(validatable)
pid := creds.GetPID()
pidUser, err := o.Authboss.Storage.Server.Load(r.Context(), pid)
if err == authboss.ErrUserNotFound {
logger.Infof("failed to load user requested by pid: %s", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
return o.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
} else if err != nil {
return err
}
otpUser := MustBeOTPable(pidUser)
passwords := decodeOTPs(otpUser.GetOTPs())
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, pidUser))
input := creds.GetPassword()
matchPassword := -1
handled := false
for i, p := range passwords {
err = bcrypt.CompareHashAndPassword([]byte(p), []byte(input))
if err == nil {
matchPassword = i
break
}
}
if matchPassword < 0 {
handled, err = o.Authboss.Events.FireAfter(authboss.EventAuthFail, w, r)
if err != nil {
return err
} else if handled {
return nil
}
logger.Infof("user %s failed to log in with otp", pid)
data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"}
return o.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data)
}
passwords[matchPassword] = passwords[len(passwords)-1]
passwords = passwords[:len(passwords)-1]
otpUser.PutOTPs(encodeOTPs(passwords))
if err = o.Authboss.Config.Storage.Server.Save(r.Context(), pidUser); err != nil {
return err
}
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyValues, validatable))
handled, err = o.Events.FireBefore(authboss.EventAuth, w, r)
if err != nil {
return err
} else if handled {
return nil
}
logger.Infof("user %s logged in via otp", pid)
authboss.PutSession(w, authboss.SessionKey, pid)
authboss.DelSession(w, authboss.SessionHalfAuthKey)
handled, err = o.Authboss.Events.FireAfter(authboss.EventAuth, w, r)
if err != nil {
return err
} else if handled {
return nil
}
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: o.Authboss.Paths.AuthLoginOK,
FollowRedirParam: true,
}
return o.Authboss.Core.Redirector.Redirect(w, r, ro)
}
// AddGet shows how many passwords exist and allows the user to create a new one
func (o *OTP) AddGet(w http.ResponseWriter, r *http.Request) error {
logger := o.RequestLogger(r)
user, err := o.Authboss.CurrentUser(r)
if err != nil {
return err
}
otpUser := MustBeOTPable(user)
ln := strconv.Itoa(len(decodeOTPs(otpUser.GetOTPs())))
return o.Core.Responder.Respond(w, r, http.StatusOK, PageAdd, authboss.HTMLData{NumberOTPS: ln})
}
// AddPost adds a new password to the user and displays it
func (o *OTP) AddPost(w http.ResponseWriter, r *http.Request) error {
logger := o.RequestLogger(r)
user, err := o.Authboss.CurrentUser(r)
if err != nil {
return err
}
// GENERATE AN OTP
panic("otp not generated")
otp := ""
otpUser := MustBeOTPable(user)
currentOTPs := decodeOTPs(otpUser.GetOTPs())
currentOTPs = append(currentOTPs, otp)
otpUser.PutOTPs(encodeOTPs(currentOTPs))
if err := o.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil {
return err
}
return o.Core.Responder.Respond(w, r, http.StatusOK, PageAdd, authboss.HTMLData{DataOTP: otp})
}
// ClearGet shows how many passwords exist and allows the user to clear them all
func (o *OTP) ClearGet(w http.ResponseWriter, r *http.Request) error {
return o.Core.Responder.Respond(w, r, http.StatusOK, PageClear, nil)
}
// ClearPost clears all otps that are stored for the user.
func (o *OTP) ClearPost(w http.ResponseWriter, r *http.Request) error {
panic("not implemented")
return nil
}
func encodeOTPs(otps []string) string {
return strings.Join(otps, ",")
}
func decodeOTPs(otps string) []string {
if len(otps) == 0 {
return nil
}
return strings.Split(otps, ",")
}

View File

@ -0,0 +1,29 @@
// Package otp allows authentication via one time passwords
package otp
import "github.com/volatiletech/authboss"
// Authenticator is a type that implements the basic functionality
// to be able to authenticate via a one time password.
type Authenticator interface {
// Initialize by giving the user a way to enter the first otp
Setup(User)
// Verify the otp for the user
Verify(User, string)
// Remove otp for the user, requires the user be fully authenticated
// and have authenticated with a one time password.
Remove(User, string)
}
// User interface
type User interface {
authboss.User
GetEmail() string
PutEmail(string)
// GetRecoveryCodes retrieves a CSV string of bcrypt'd recovery codes
GetRecoveryCodes() string
// PutRecoveryCodes uses a single string to store many bcrypt'd recovery codes
PutRecoveryCodes(codes string)
}

View File

@ -7,6 +7,13 @@ import (
"github.com/pkg/errors"
)
const (
// FormValueRedirect should be honored by HTTPRedirector implementations
// as the value from the URL that overrides the typical redirect when
// FollowRedirParam is set to true.
FormValueRedirect = "redir"
)
// HTTPResponder knows how to respond to an HTTP request
// Must consider:
// - Flash messages