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) } } }