mirror of
https://github.com/volatiletech/authboss.git
synced 2025-02-07 13:41:55 +02:00
WIP
This commit is contained in:
parent
807a692e26
commit
48e83e1a2a
@ -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)
|
||||
}
|
||||
|
47
authboss.go
47
authboss.go
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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 = "/"
|
||||
|
@ -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
232
otp/otp.go
Normal 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, ",")
|
||||
}
|
29
otp/twofactor/twofactor.go
Normal file
29
otp/twofactor/twofactor.go
Normal 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)
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user