3
0
corteza/auth/handlers/handler.go
2021-03-07 18:58:16 +01:00

347 lines
9.7 KiB
Go

package handlers
import (
"context"
"encoding/gob"
"github.com/cortezaproject/corteza-server/auth/external"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/settings"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/options"
"github.com/cortezaproject/corteza-server/system/types"
oauth2server "github.com/go-oauth2/oauth2/v4/server"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/markbates/goth"
"go.uber.org/zap"
"io"
"net/http"
"net/url"
"sort"
"strings"
)
type (
authService interface {
External(ctx context.Context, profile goth.User) (u *types.User, err error)
InternalSignUp(ctx context.Context, input *types.User, password string) (u *types.User, err error)
InternalLogin(ctx context.Context, email string, password string) (u *types.User, err error)
SetPassword(ctx context.Context, userID uint64, password string) (err error)
ChangePassword(ctx context.Context, userID uint64, oldPassword, newPassword string) (err error)
ValidateEmailConfirmationToken(ctx context.Context, token string) (user *types.User, err error)
ValidatePasswordResetToken(ctx context.Context, token string) (user *types.User, err error)
SendEmailAddressConfirmationToken(ctx context.Context, u *types.User) (err error)
SendPasswordResetToken(ctx context.Context, email string) (err error)
GetProviders() types.ExternalAuthProviderSet
ValidateTOTP(ctx context.Context, code string) (err error)
ConfigureTOTP(ctx context.Context, secret string, code string) (u *types.User, err error)
RemoveTOTP(ctx context.Context, userID uint64, code string) (u *types.User, err error)
SendEmailOTP(ctx context.Context) (err error)
ConfigureEmailOTP(ctx context.Context, userID uint64, enable bool) (u *types.User, err error)
ValidateEmailOTP(ctx context.Context, code string) (err error)
}
userService interface {
Update(context.Context, *types.User) (*types.User, error)
}
clientService interface {
LookupByID(context.Context, uint64) (*types.AuthClient, error)
Confirmed(context.Context, uint64) (types.AuthConfirmedClientSet, error)
Revoke(ctx context.Context, userID, clientID uint64) error
}
// @todo this should probably be a little more decoupled from the store and nicely named
tokenService interface {
SearchByUserID(ctx context.Context, userID uint64) (types.AuthOa2tokenSet, error)
DeleteByID(ctx context.Context, ID uint64) error
DeleteByUserID(ctx context.Context, userID uint64) error
}
templateExecutor interface {
ExecuteTemplate(io.Writer, string, interface{}) error
}
AuthHandlers struct {
Log *zap.Logger
Templates templateExecutor
OAuth2 *oauth2server.Server
SessionManager *request.SessionManager
AuthService authService
UserService userService
ClientService clientService
TokenService tokenService
DefaultClient *types.AuthClient
Opt options.AuthOpt
Settings *settings.Settings
}
handlerFn func(req *request.AuthReq) error
)
const (
TmplAuthorizedClients = "authorized-clients.html.tpl"
TmplChangePassword = "change-password.html.tpl"
TmplLogin = "login.html.tpl"
TmplLogout = "logout.html.tpl"
TmplOAuth2AuthorizeClient = "oauth2-authorize-client.html.tpl"
TmplRequestPasswordReset = "request-password-reset.html.tpl"
TmplPasswordResetRequested = "password-reset-requested.html.tpl"
TmplResetPassword = "reset-password.html.tpl"
TmplSecurity = "security.html.tpl"
TmplProfile = "profile.html.tpl"
TmplSessions = "sessions.html.tpl"
TmplSignup = "signup.html.tpl"
TmplPendingEmailConfirmation = "pending-email-confirmation.html.tpl"
TmplMfa = "mfa.html.tpl"
TmplMfaTotp = "mfa-totp.html.tpl"
TmplMfaTotpDisable = "mfa-totp-disable.html.tpl"
TmplInternalError = "error-internal.html.tpl"
)
func init() {
gob.Register(&types.User{})
gob.Register(&types.AuthClient{})
gob.Register([]request.Alert{})
gob.Register(url.Values{})
}
// handles auth request and prepares request struct with request, session and response helper
func (h *AuthHandlers) handle(fn handlerFn) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
req = &request.AuthReq{
Response: w,
Request: r,
Data: make(map[string]interface{}),
NewAlerts: make([]request.Alert, 0),
PrevAlerts: make([]request.Alert, 0),
Session: h.SessionManager.Get(r),
}
)
h.Log.Debug(
"handling request",
zap.String("url", r.RequestURI),
zap.String("method", r.Method),
)
err := func() (err error) {
if err = r.ParseForm(); err != nil {
return
}
req.Client = request.GetOauth2Client(req.Session)
req.AuthUser = request.GetAuthUser(req.Session)
// make sure user (identity) is part of the context
// so we can properly identify ourselves when interacting
// with services
if req.AuthUser != nil && !req.AuthUser.PendingMFA() {
req.Request = req.Request.Clone(auth.SetIdentityToContext(req.Context(), req.AuthUser.User))
}
// Alerts show for 1 session only!
req.PrevAlerts = req.PopAlerts()
if err = fn(req); err != nil {
h.Log.Error("error in handler", zap.Error(err))
return
}
if req.RedirectTo != "" && len(req.PrevAlerts) > 0 {
// redirect happened, so probably none noticed alerts
// lets push them at the end of new alerts
req.NewAlerts = append(req.NewAlerts, req.PrevAlerts...)
}
if len(req.NewAlerts) > 0 {
req.SetAlerts(req.NewAlerts...)
}
if err = sessions.Save(r, w); err != nil {
h.Log.Error("could not save session", zap.Error(err))
}
if req.Status == 0 {
switch {
case req.RedirectTo != "":
req.Status = http.StatusSeeOther
req.Template = ""
case req.Template != "":
req.Status = http.StatusOK
default:
req.Status = http.StatusInternalServerError
req.Template = TmplInternalError
}
}
return nil
}()
if err == nil {
if req.Status >= 300 && req.Status < 400 {
// redirect, nothing special to handle
http.Redirect(w, r, req.RedirectTo, req.Status)
return
}
if req.Status > 0 {
// in cases when something else already wrote the status
w.WriteHeader(req.Status)
}
}
// Handling just text/html response types from here on
//
// If handler does not wish to use the template leave/set it to "" (empty string)
if err == nil && req.Template != "" {
err = h.Templates.ExecuteTemplate(w, req.Template, h.enrichTmplData(req))
h.Log.Debug("template executed", zap.String("name", req.Template), zap.Error(err))
}
if err != nil {
err = h.Templates.ExecuteTemplate(w, TmplInternalError, map[string]interface{}{
"error": err,
})
if err == nil {
return
}
}
if err != nil {
h.Log.Error("unhandled error", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
// Add alerts, settings, providers, csrf token
func (h *AuthHandlers) enrichTmplData(req *request.AuthReq) interface{} {
d := req.Data
if req.AuthUser != nil {
req.Data["user"] = req.AuthUser.User
}
if req.Client != nil {
c := authClient{
ID: req.Client.ID,
Name: req.Client.Handle,
}
if req.Client.Meta != nil {
c.Name = req.Client.Meta.Name
c.Description = req.Client.Meta.Description
}
req.Data["client"] = c
}
d[csrf.TemplateTag] = csrf.TemplateField(req.Request)
// In case we did not redirect, join previous alerts with new ones
d["alerts"] = append(req.PrevAlerts, req.NewAlerts...)
dSettings := *h.Settings
dSettings.Providers = nil
d["settings"] = dSettings
providers := h.AuthService.GetProviders()
sort.Sort(providers)
var pp = make([]provider, 0, len(providers))
for i := range providers {
if !providers[i].Enabled {
continue
}
p := provider{
Label: providers[i].Label,
Handle: providers[i].Handle,
Icon: providers[i].Handle,
}
if strings.HasPrefix(p.Icon, external.OIDC_PROVIDER_PREFIX) {
p.Icon = "key"
}
pp = append(pp, p)
}
d["providers"] = pp
return d
}
// Handle successful auth (on any factor)
func handleSuccessfulAuth(req *request.AuthReq) {
switch {
case req.AuthUser.PendingMFA():
req.RedirectTo = GetLinks().Mfa
case request.GetOAuth2AuthParams(req.Session) != nil:
// client authorization flow was paused, continue.
req.RedirectTo = GetLinks().OAuth2AuthorizeClient
default:
// Always go to profile
req.RedirectTo = GetLinks().Profile
}
}
// redirects anonymous users to login
func authOnly(fn handlerFn) handlerFn {
return func(req *request.AuthReq) error {
// these next few lines keep users away from the pages they should not see
// and redirect them to where they need to be
switch {
case req.AuthUser == nil || req.AuthUser.User == nil:
// not authenticated at all, move to login
req.RedirectTo = GetLinks().Login
case req.AuthUser.UnconfiguredTOTP():
// authenticated but need to configure MFA
req.RedirectTo = GetLinks().MfaTotpNewSecret
case req.AuthUser.PendingMFA():
// authenticated but MFA pending
req.RedirectTo = GetLinks().Mfa
default:
return fn(req)
}
return nil
}
}
func partAuthOnly(fn handlerFn) handlerFn {
return func(req *request.AuthReq) error {
if req.AuthUser == nil || req.AuthUser.User == nil {
req.RedirectTo = GetLinks().Login
return nil
} else {
return fn(req)
}
}
}
// redirects authenticated users to profile
func anonyOnly(fn handlerFn) handlerFn {
return func(req *request.AuthReq) error {
if req.AuthUser != nil && req.AuthUser.User != nil {
req.RedirectTo = GetLinks().Profile
return nil
} else {
return fn(req)
}
}
}