3
0
Files
corteza/server/auth/handlers/handler.go

495 lines
15 KiB
Go

package handlers
import (
"context"
"encoding/gob"
"fmt"
"html/template"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/auth/external"
"github.com/cortezaproject/corteza/server/auth/request"
"github.com/cortezaproject/corteza/server/auth/saml"
"github.com/cortezaproject/corteza/server/auth/settings"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/markbates/goth"
"github.com/spf13/cast"
"go.uber.org/zap"
)
type (
authService interface {
External(ctx context.Context, profile types.ExternalAuthUser) (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)
ValidatePasswordCreateToken(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
PasswordSet(ctx context.Context, email string) (is bool)
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)
LoadRoleMemberships(ctx context.Context, u *types.User) 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)
}
credentialsService interface {
List(ctx context.Context, userID uint64) (cc types.CredentialSet, err error)
Create(ctx context.Context, c *types.Credential) (*types.Credential, error)
Update(ctx context.Context, c *types.Credential) (*types.Credential, error)
Delete(ctx context.Context, userID, credentialsID uint64) (err error)
}
userService interface {
FindByAny(ctx context.Context, identifier interface{}) (*types.User, error)
Update(context.Context, *types.User) (*types.User, error)
UploadAvatar(ctx context.Context, userID uint64, upload *multipart.FileHeader) (err error)
GenerateAvatar(ctx context.Context, userID uint64, bgColor string, initialColor string) (err error)
DeleteAvatar(ctx context.Context, userID uint64) (err error)
}
clientService interface {
Lookup(context.Context, interface{}) (*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{}, ...func(template *template.Template) *template.Template) error
}
oauth2Service interface {
GetRedirectURI(req *server.AuthorizeRequest, data map[string]interface{}) (string, error)
CheckResponseType(rt oauth2.ResponseType) bool
CheckCodeChallengeMethod(ccm oauth2.CodeChallengeMethod) bool
ValidationAuthorizeRequest(r *http.Request) (*server.AuthorizeRequest, error)
GetAuthorizeToken(ctx context.Context, req *server.AuthorizeRequest) (oauth2.TokenInfo, error)
GetAuthorizeData(rt oauth2.ResponseType, ti oauth2.TokenInfo) map[string]interface{}
HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) error
ValidationTokenRequest(r *http.Request) (oauth2.GrantType, *oauth2.TokenGenerateRequest, error)
CheckGrantType(gt oauth2.GrantType) bool
GetAccessToken(ctx context.Context, gt oauth2.GrantType, tgr *oauth2.TokenGenerateRequest) (oauth2.TokenInfo, error)
GetTokenData(ti oauth2.TokenInfo) map[string]interface{}
HandleTokenRequest(w http.ResponseWriter, r *http.Request) error
GetErrorData(err error) (map[string]interface{}, int, http.Header)
BearerAuth(r *http.Request) (string, bool)
ValidationBearerToken(r *http.Request) (oauth2.TokenInfo, error)
}
localeService interface {
NS(ctx context.Context, ns string) func(key string, rr ...string) string
T(ctx context.Context, ns, key string, rr ...string) string
LocalizedList(ctx context.Context) []*locale.Language
}
AuthHandlers struct {
Log *zap.Logger
Locale localeService
Templates templateExecutor
OAuth2 oauth2Service
SessionManager *request.SessionManager
AuthService authService
CredentialsService credentialsService
UserService userService
ClientService clientService
TokenService tokenService
DefaultClient *types.AuthClient
Opt options.AuthOpt
Settings *settings.Settings
SamlSPService *saml.SamlSPService
Attachment service.AttachmentService
}
handlerFn func(req *request.AuthReq) error
)
const (
TmplAuthorizedClients = "authorized-clients.html.tpl"
TmplChangePassword = "change-password.html.tpl"
TmplCreatePassword = "create-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"
// 1k of data per POST field is all we allow
maxPostValueLength = 2 << 9
// general limitation on number of fields
maxPostFields = 10
)
var (
// wrapper around time.Now() that will aid service testing
now = func() *time.Time {
c := time.Now()
return &c
}
)
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),
Locale: h.Locale,
}
)
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
}
if !validFormPost(r) {
req.Status = http.StatusRequestEntityTooLarge
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(),
auth.Authenticated(req.AuthUser.User.ID, req.AuthUser.User.Roles()...),
))
}
// 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))
}
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)
}
}
var (
// translator template function
//
// we're adding these functions on every request on all templates
// because we need request's context to detect the language from!
ttf = func(t *template.Template) *template.Template {
return t.Funcs(map[string]interface{}{
"language": func() string { return locale.GetAcceptLanguageFromContext(req.Context()).String() },
"tr": func(key string, pp ...interface{}) template.HTML {
ss := make([]string, len(pp))
for i := range pp {
ss[i] = cast.ToString(pp[i])
}
return template.HTML(h.Locale.T(req.Context(), "auth", key, ss...))
},
})
}
)
// 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),
ttf,
)
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,
}, ttf)
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, Bg
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
var pp = make([]provider, 0, len(dSettings.Providers))
if h.Settings.Saml.Enabled {
pp = append(pp, provider(saml.TemplateProvider(h.Settings.Saml.IDP.URL, h.Settings.Saml.Name)))
}
for _, p := range dSettings.Providers {
if _, err := goth.GetProvider(p.Handle); err != nil {
continue
}
// Skipping the ones we don't use for identity
if !p.HasUsage(types.ExternalProviderUsageIdentity) {
continue
}
out := provider{
Label: p.Label,
Handle: p.Handle,
Icon: p.Handle,
}
if strings.HasPrefix(out.Icon, external.OIDC_PROVIDER_PREFIX) {
out.Icon = "key"
}
pp = append(pp, out)
}
d["providers"] = pp
dSettings.Providers = nil
d["settings"] = dSettings
d["authBg"] = h.bgStylesData()
return d
}
func (h *AuthHandlers) bgStylesData() string {
if h.Settings.BackgroundUI.BackgroundImageSrcUrl == "" {
return fmt.Sprintf("background: url(%s/release-background.png) no-repeat top; %s",
GetLinks().Assets, h.Settings.BackgroundUI.Styles)
}
return fmt.Sprintf("background: url('%s') no-repeat top; %s",
h.Settings.BackgroundUI.BackgroundImageSrcUrl, h.Settings.BackgroundUI.Styles)
}
// 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)
}
}
}
func translator(req *request.AuthReq, ns string) func(key string, rr ...string) string {
return req.Locale.NS(req.Context(), ns)
}
// general validation of posted data
//
// quite primitive for now but should be effective against out-of-bounds attacks
//
// in the future, more sophisticated validation might be needed
func validFormPost(r *http.Request) bool {
if len(r.Form) > maxPostFields {
// auth does not have any large forms
return false
}
// None of the values from the post fields should be longer than max length
for k, _ := range r.Form {
if len(r.Form[k]) > 1 {
// assuming only one value per field!
return false
}
if len(r.Form[k][0]) > maxPostValueLength {
return false
}
}
return true
}