12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418 |
- package login
- import (
- "context"
- "errors"
- "fmt"
- "io/fs"
- "net/http"
- "net/url"
- "reflect"
- "strings"
- "time"
- "github.com/golang-jwt/jwt/v4"
- "github.com/markbates/goth"
- "github.com/markbates/goth/gothic"
- "github.com/pquerna/otp"
- "github.com/pquerna/otp/totp"
- "github.com/qor5/web"
- "github.com/qor5/x/i18n"
- h "github.com/theplant/htmlgo"
- "golang.org/x/text/language"
- "gorm.io/gorm"
- )
- var (
- ErrUserNotFound = errors.New("user not found")
- ErrPasswordChanged = errors.New("password changed")
- ErrWrongPassword = errors.New("wrong password")
- ErrUserLocked = errors.New("user locked")
- ErrUserGetLocked = errors.New("user get locked")
- ErrWrongTOTPCode = errors.New("wrong totp code")
- ErrTOTPCodeHasBeenUsed = errors.New("totp code has been used")
- ErrEmptyPassword = errors.New("empty password")
- ErrPasswordNotMatch = errors.New("password not match")
- )
- type HomeURLFunc func(r *http.Request, user interface{}) string
- type HookFunc func(r *http.Request, user interface{}, extraVals ...interface{}) error
- type Provider struct {
- Goth goth.Provider
- Key string
- Text string
- Logo h.HTMLComponent
- }
- type CookieConfig struct {
- Path string
- Domain string
- SameSite http.SameSite
- }
- type TOTPConfig struct {
- Issuer string
- }
- type RecaptchaConfig struct {
- SiteKey string
- SecretKey string
- }
- type Builder struct {
- secret string
- providers []*Provider
- authCookieName string
- authSecureCookieName string
- continueUrlCookieName string
- // seconds
- sessionMaxAge int
- cookieConfig CookieConfig
- totpEnabled bool
- totpConfig TOTPConfig
- recaptchaEnabled bool
- recaptchaConfig RecaptchaConfig
- autoExtendSession bool
- maxRetryCount int
- noForgetPasswordLink bool
- i18nBuilder *i18n.Builder
- // Common URLs
- homePageURLFunc HomeURLFunc
- loginPageURL string
- LogoutURL string
- // TOTP URLs
- validateTOTPURL string
- totpSetupPageURL string
- totpValidatePageURL string
- // OAuth URLs
- oauthBeginURL string
- oauthCallbackURL string
- oauthCallbackCompleteURL string
- // UserPass URLs
- passwordLoginURL string
- resetPasswordURL string
- resetPasswordPageURL string
- changePasswordURL string
- changePasswordPageURL string
- forgetPasswordPageURL string
- sendResetPasswordLinkURL string
- resetPasswordLinkSentPageURL string
- loginPageFunc web.PageFunc
- forgetPasswordPageFunc web.PageFunc
- resetPasswordLinkSentPageFunc web.PageFunc
- resetPasswordPageFunc web.PageFunc
- changePasswordPageFunc web.PageFunc
- totpSetupPageFunc web.PageFunc
- totpValidatePageFunc web.PageFunc
- beforeSetPasswordHook HookFunc
- afterLoginHook HookFunc
- afterFailedToLoginHook HookFunc
- afterUserLockedHook HookFunc
- afterLogoutHook HookFunc
- afterConfirmSendResetPasswordLinkHook HookFunc
- afterResetPasswordHook HookFunc
- afterChangePasswordHook HookFunc
- afterExtendSessionHook HookFunc
- afterTOTPCodeReusedHook HookFunc
- afterOAuthCompleteHook HookFunc
- db *gorm.DB
- userModel interface{}
- snakePrimaryField string
- tUser reflect.Type
- userPassEnabled bool
- oauthEnabled bool
- sessionSecureEnabled bool
- }
- func New() *Builder {
- r := &Builder{
- authCookieName: "auth",
- authSecureCookieName: "qor5_auth_secure",
- continueUrlCookieName: "qor5_continue_url",
- homePageURLFunc: func(r *http.Request, user interface{}) string {
- return "/"
- },
- loginPageURL: "/auth/login",
- LogoutURL: "/auth/logout",
- validateTOTPURL: "/auth/2fa/totp/do",
- totpSetupPageURL: "/auth/2fa/totp/setup",
- totpValidatePageURL: "/auth/2fa/totp/validate",
- oauthBeginURL: "/auth/begin",
- oauthCallbackURL: "/auth/callback",
- oauthCallbackCompleteURL: "/auth/callback-complete",
- passwordLoginURL: "/auth/userpass/login",
- resetPasswordURL: "/auth/do-reset-password",
- resetPasswordPageURL: "/auth/reset-password",
- changePasswordURL: "/auth/do-change-password",
- changePasswordPageURL: "/auth/change-password",
- forgetPasswordPageURL: "/auth/forget-password",
- sendResetPasswordLinkURL: "/auth/send-reset-password-link",
- resetPasswordLinkSentPageURL: "/auth/reset-password-link-sent",
- sessionMaxAge: 60 * 60,
- cookieConfig: CookieConfig{
- Path: "/",
- Domain: "",
- SameSite: http.SameSiteStrictMode,
- },
- autoExtendSession: true,
- maxRetryCount: 5,
- totpEnabled: true,
- totpConfig: TOTPConfig{
- Issuer: "QOR5",
- },
- }
- i18nB := i18n.New()
- i18nB.SupportLanguages(language.English, language.SimplifiedChinese, language.Japanese)
- r.I18n(i18nB)
- vh := r.ViewHelper()
- r.loginPageFunc = defaultLoginPage(vh)
- r.forgetPasswordPageFunc = defaultForgetPasswordPage(vh)
- r.resetPasswordLinkSentPageFunc = defaultResetPasswordLinkSentPage(vh)
- r.resetPasswordPageFunc = defaultResetPasswordPage(vh)
- r.changePasswordPageFunc = defaultChangePasswordPage(vh)
- r.totpSetupPageFunc = defaultTOTPSetupPage(vh)
- r.totpValidatePageFunc = defaultTOTPValidatePage(vh)
- return r
- }
- func (b *Builder) Secret(v string) (r *Builder) {
- b.secret = v
- return b
- }
- func (b *Builder) CookieConfig(v CookieConfig) (r *Builder) {
- b.cookieConfig = v
- return b
- }
- // Google reCAPTCHA.
- func (b *Builder) Recaptcha(enable bool, config ...RecaptchaConfig) (r *Builder) {
- b.recaptchaEnabled = enable
- if len(config) > 0 {
- b.recaptchaConfig = config[0]
- }
- if enable {
- if b.recaptchaConfig.SiteKey == "" {
- panic("SiteKey is empty")
- }
- if b.recaptchaConfig.SecretKey == "" {
- panic("SecretKey is empty")
- }
- }
- return b
- }
- func (b *Builder) OAuthProviders(vs ...*Provider) (r *Builder) {
- if len(vs) == 0 {
- return b
- }
- b.oauthEnabled = true
- b.providers = vs
- var gothProviders []goth.Provider
- for _, v := range vs {
- gothProviders = append(gothProviders, v.Goth)
- }
- goth.UseProviders(gothProviders...)
- return b
- }
- func (b *Builder) AuthCookieName(v string) (r *Builder) {
- b.authCookieName = v
- return b
- }
- func (b *Builder) HomeURLFunc(v HomeURLFunc) (r *Builder) {
- b.homePageURLFunc = v
- return b
- }
- func (b *Builder) LoginPageURL(v string) (r *Builder) {
- b.loginPageURL = v
- return b
- }
- func (b *Builder) ResetPasswordPageURL(v string) (r *Builder) {
- b.resetPasswordPageURL = v
- return b
- }
- func (b *Builder) ChangePasswordPageURL(v string) (r *Builder) {
- b.changePasswordPageURL = v
- return b
- }
- func (b *Builder) ForgetPasswordPageURL(v string) (r *Builder) {
- b.forgetPasswordPageURL = v
- return b
- }
- func (b *Builder) ResetPasswordLinkSentPageURL(v string) (r *Builder) {
- b.resetPasswordLinkSentPageURL = v
- return b
- }
- func (b *Builder) TOTPSetupPageURL(v string) (r *Builder) {
- b.totpSetupPageURL = v
- return b
- }
- func (b *Builder) TOTPValidatePageURL(v string) (r *Builder) {
- b.totpValidatePageURL = v
- return b
- }
- func (b *Builder) LoginPageFunc(v web.PageFunc) (r *Builder) {
- b.loginPageFunc = v
- return b
- }
- func (b *Builder) ForgetPasswordPageFunc(v web.PageFunc) (r *Builder) {
- b.forgetPasswordPageFunc = v
- return b
- }
- func (b *Builder) ResetPasswordLinkSentPageFunc(v web.PageFunc) (r *Builder) {
- b.resetPasswordLinkSentPageFunc = v
- return b
- }
- func (b *Builder) ResetPasswordPageFunc(v web.PageFunc) (r *Builder) {
- b.resetPasswordPageFunc = v
- return b
- }
- func (b *Builder) ChangePasswordPageFunc(v web.PageFunc) (r *Builder) {
- b.changePasswordPageFunc = v
- return b
- }
- func (b *Builder) TOTPSetupPageFunc(v web.PageFunc) (r *Builder) {
- b.totpSetupPageFunc = v
- return b
- }
- func (b *Builder) TOTPValidatePageFunc(v web.PageFunc) (r *Builder) {
- b.totpValidatePageFunc = v
- return b
- }
- func (b *Builder) wrapHook(v HookFunc) HookFunc {
- if v == nil {
- return nil
- }
- return func(r *http.Request, user interface{}, extraVals ...interface{}) error {
- if user != nil && GetCurrentUser(r) == nil {
- r = r.WithContext(context.WithValue(r.Context(), UserKey, user))
- }
- return v(r, user, extraVals...)
- }
- }
- // extra vals:
- // - password
- func (b *Builder) BeforeSetPassword(v HookFunc) (r *Builder) {
- b.beforeSetPasswordHook = b.wrapHook(v)
- return b
- }
- func (b *Builder) AfterLogin(v HookFunc) (r *Builder) {
- b.afterLoginHook = b.wrapHook(v)
- return b
- }
- // extra vals:
- // - login error
- func (b *Builder) AfterFailedToLogin(v HookFunc) (r *Builder) {
- b.afterFailedToLoginHook = b.wrapHook(v)
- return b
- }
- func (b *Builder) AfterUserLocked(v HookFunc) (r *Builder) {
- b.afterUserLockedHook = b.wrapHook(v)
- return b
- }
- func (b *Builder) AfterLogout(v HookFunc) (r *Builder) {
- b.afterLogoutHook = b.wrapHook(v)
- return b
- }
- // extra vals:
- // - reset link
- func (b *Builder) AfterConfirmSendResetPasswordLink(v HookFunc) (r *Builder) {
- b.afterConfirmSendResetPasswordLinkHook = b.wrapHook(v)
- return b
- }
- func (b *Builder) AfterResetPassword(v HookFunc) (r *Builder) {
- b.afterResetPasswordHook = b.wrapHook(v)
- return b
- }
- func (b *Builder) AfterChangePassword(v HookFunc) (r *Builder) {
- b.afterChangePasswordHook = b.wrapHook(v)
- return b
- }
- // extra vals:
- // - old session token
- func (b *Builder) AfterExtendSession(v HookFunc) (r *Builder) {
- b.afterExtendSessionHook = b.wrapHook(v)
- return b
- }
- func (b *Builder) AfterTOTPCodeReused(v HookFunc) (r *Builder) {
- b.afterTOTPCodeReusedHook = b.wrapHook(v)
- return b
- }
- // user is goth.User
- func (b *Builder) AfterOAuthComplete(v HookFunc) (r *Builder) {
- b.afterOAuthCompleteHook = b.wrapHook(v)
- return b
- }
- // seconds
- // default 1h
- func (b *Builder) SessionMaxAge(v int) (r *Builder) {
- b.sessionMaxAge = v
- return b
- }
- // extend the session if successfully authenticated
- // default true
- func (b *Builder) AutoExtendSession(v bool) (r *Builder) {
- b.autoExtendSession = v
- return b
- }
- // default 5
- // MaxRetryCount <= 0 means no max retry count limit
- func (b *Builder) MaxRetryCount(v int) (r *Builder) {
- b.maxRetryCount = v
- return b
- }
- func (b *Builder) TOTP(enable bool, config ...TOTPConfig) (r *Builder) {
- b.totpEnabled = enable
- if len(config) > 0 {
- b.totpConfig = config[0]
- }
- if enable {
- if b.totpConfig.Issuer == "" {
- panic("Issuer is empty")
- }
- }
- return b
- }
- func (b *Builder) NoForgetPasswordLink(v bool) (r *Builder) {
- b.noForgetPasswordLink = v
- return b
- }
- func (b *Builder) DB(v *gorm.DB) (r *Builder) {
- b.db = v
- return b
- }
- func (b *Builder) I18n(v *i18n.Builder) (r *Builder) {
- v.RegisterForModule(language.English, I18nLoginKey, Messages_en_US).
- RegisterForModule(language.SimplifiedChinese, I18nLoginKey, Messages_zh_CN).
- RegisterForModule(language.Japanese, I18nLoginKey, Messages_ja_JP)
- b.i18nBuilder = v
- return b
- }
- func (b *Builder) GetSessionMaxAge() int {
- return b.sessionMaxAge
- }
- func (b *Builder) ViewHelper() *ViewHelper {
- return &ViewHelper{
- b: b,
- }
- }
- func (b *Builder) UserModel(m interface{}) (r *Builder) {
- b.userModel = m
- b.tUser = underlyingReflectType(reflect.TypeOf(m))
- b.snakePrimaryField = snakePrimaryField(m)
- if _, ok := m.(UserPasser); ok {
- b.userPassEnabled = true
- }
- if _, ok := m.(OAuthUser); ok {
- b.oauthEnabled = true
- }
- if _, ok := m.(SessionSecurer); ok {
- b.sessionSecureEnabled = true
- }
- return b
- }
- func (b *Builder) newUserObject() interface{} {
- return reflect.New(b.tUser).Interface()
- }
- func (b *Builder) findUserByID(id string) (user interface{}, err error) {
- m := b.newUserObject()
- err = b.db.Where(fmt.Sprintf("%s = ?", b.snakePrimaryField), id).
- First(m).
- Error
- if err != nil {
- if err == gorm.ErrRecordNotFound {
- return nil, ErrUserNotFound
- }
- return nil, err
- }
- return m, nil
- }
- // completeUserAuthCallback is for url "/auth/{provider}/callback"
- func (b *Builder) completeUserAuthCallback(w http.ResponseWriter, r *http.Request) {
- if b.cookieConfig.SameSite != http.SameSiteStrictMode {
- b.completeUserAuthCallbackComplete(w, r)
- return
- }
- completeURL := fmt.Sprintf("%s?%s", b.oauthCallbackCompleteURL, r.URL.Query().Encode())
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.Write([]byte(fmt.Sprintf(`
- <script>
- window.location.href="%s";
- </script>
- <a href="%s">complete</a>
- `, completeURL, completeURL)))
- return
- }
- func (b *Builder) completeUserAuthCallbackComplete(w http.ResponseWriter, r *http.Request) {
- var err error
- var user interface{}
- failRedirectURL := b.LogoutURL
- defer func() {
- if perr := recover(); perr != nil {
- panic(perr)
- }
- if err != nil {
- if b.afterFailedToLoginHook != nil {
- if herr := b.afterFailedToLoginHook(r, user, err); herr != nil {
- setNoticeOrPanic(w, herr)
- }
- }
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- }
- }()
- var ouser goth.User
- ouser, err = gothic.CompleteUserAuth(w, r)
- if err != nil {
- setFailCodeFlash(w, FailCodeCompleteUserAuthFailed)
- return
- }
- if b.afterOAuthCompleteHook != nil {
- if err = b.afterOAuthCompleteHook(r, ouser); err != nil {
- setNoticeOrPanic(w, err)
- return
- }
- }
- userID := ouser.UserID
- if b.userModel != nil {
- user, err = b.userModel.(OAuthUser).FindUserByOAuthUserID(b.db, b.newUserObject(), ouser.Provider, ouser.UserID)
- if err != nil {
- if err != gorm.ErrRecordNotFound {
- panic(err)
- }
- // TODO: maybe the identifier of some providers is not email
- identifier := ouser.Email
- user, err = b.userModel.(OAuthUser).FindUserByOAuthIdentifier(b.db, b.newUserObject(), ouser.Provider, identifier)
- if err != nil {
- if err != gorm.ErrRecordNotFound {
- panic(err)
- }
- setFailCodeFlash(w, FailCodeUserNotFound)
- return
- }
- err = user.(OAuthUser).InitOAuthUserID(b.db, b.newUserObject(), ouser.Provider, identifier, ouser.UserID)
- if err != nil {
- panic(err)
- }
- }
- userID = objectID(user)
- }
- claims := UserClaims{
- Provider: ouser.Provider,
- Email: ouser.Email,
- Name: ouser.Name,
- UserID: userID,
- AvatarURL: ouser.AvatarURL,
- RegisteredClaims: b.genBaseSessionClaim(userID),
- }
- if user == nil {
- user = &claims
- }
- if b.afterLoginHook != nil {
- setCookieForRequest(r, &http.Cookie{Name: b.authCookieName, Value: b.mustGetSessionToken(claims)})
- if err = b.afterLoginHook(r, user); err != nil {
- setNoticeOrPanic(w, err)
- return
- }
- }
- if err = b.setSecureCookiesByClaims(w, user, claims); err != nil {
- panic(err)
- }
- redirectURL := b.homePageURLFunc(r, user)
- if v := b.getContinueURL(w, r); v != "" {
- redirectURL = v
- }
- http.Redirect(w, r, redirectURL, http.StatusFound)
- return
- }
- // return user if account exists even if there is an error returned
- func (b *Builder) authUserPass(account string, password string) (user interface{}, err error) {
- user, err = b.userModel.(UserPasser).FindUser(b.db, b.newUserObject(), account)
- if err != nil {
- if err == gorm.ErrRecordNotFound {
- return nil, ErrUserNotFound
- }
- return nil, err
- }
- u := user.(UserPasser)
- if u.GetLocked() {
- return user, ErrUserLocked
- }
- if !u.IsPasswordCorrect(password) {
- if b.maxRetryCount > 0 {
- if err = u.IncreaseRetryCount(b.db, b.newUserObject()); err != nil {
- return user, err
- }
- if u.GetLoginRetryCount() >= b.maxRetryCount {
- if err = u.LockUser(b.db, b.newUserObject()); err != nil {
- return user, err
- }
- return user, ErrUserGetLocked
- }
- }
- return user, ErrWrongPassword
- }
- if u.GetLoginRetryCount() != 0 {
- if err = u.UnlockUser(b.db, b.newUserObject()); err != nil {
- return user, err
- }
- }
- return user, nil
- }
- func (b *Builder) userpassLogin(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- // check reCAPTCHA token
- if b.recaptchaEnabled {
- token := r.FormValue("token")
- if !recaptchaTokenCheck(b, token) {
- setFailCodeFlash(w, FailCodeIncorrectRecaptchaToken)
- http.Redirect(w, r, b.loginPageURL, http.StatusFound)
- return
- }
- }
- var err error
- var user interface{}
- failRedirectURL := b.LogoutURL
- defer func() {
- if perr := recover(); perr != nil {
- panic(perr)
- }
- if err != nil {
- if b.afterFailedToLoginHook != nil {
- if herr := b.afterFailedToLoginHook(r, user, err); herr != nil {
- setNoticeOrPanic(w, herr)
- }
- }
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- }
- }()
- account := r.FormValue("account")
- password := r.FormValue("password")
- user, err = b.authUserPass(account, password)
- if err != nil {
- if err == ErrUserGetLocked && b.afterUserLockedHook != nil {
- if err = b.afterUserLockedHook(r, user); err != nil {
- setNoticeOrPanic(w, err)
- return
- }
- }
- var code FailCode
- switch err {
- case ErrWrongPassword, ErrUserNotFound:
- code = FailCodeIncorrectAccountNameOrPassword
- case ErrUserLocked, ErrUserGetLocked:
- code = FailCodeUserLocked
- default:
- panic(err)
- }
- setFailCodeFlash(w, code)
- setWrongLoginInputFlash(w, WrongLoginInputFlash{
- Account: account,
- Password: password,
- })
- return
- }
- u := user.(UserPasser)
- userID := objectID(user)
- claims := UserClaims{
- UserID: userID,
- PassUpdatedAt: u.GetPasswordUpdatedAt(),
- RegisteredClaims: b.genBaseSessionClaim(userID),
- }
- if !b.totpEnabled {
- if b.afterLoginHook != nil {
- setCookieForRequest(r, &http.Cookie{Name: b.authCookieName, Value: b.mustGetSessionToken(claims)})
- if err = b.afterLoginHook(r, user); err != nil {
- setNoticeOrPanic(w, err)
- return
- }
- }
- }
- if err = b.setSecureCookiesByClaims(w, user, claims); err != nil {
- panic(err)
- }
- if b.totpEnabled {
- if u.GetIsTOTPSetup() {
- http.Redirect(w, r, b.totpValidatePageURL, http.StatusFound)
- return
- }
- var key *otp.Key
- if key, err = totp.Generate(
- totp.GenerateOpts{
- Issuer: b.totpConfig.Issuer,
- AccountName: u.GetAccountName(),
- },
- ); err != nil {
- panic(err)
- }
- if err = u.SetTOTPSecret(b.db, b.newUserObject(), key.Secret()); err != nil {
- panic(err)
- }
- http.Redirect(w, r, b.totpSetupPageURL, http.StatusFound)
- return
- }
- redirectURL := b.homePageURLFunc(r, user)
- if v := b.getContinueURL(w, r); v != "" {
- redirectURL = v
- }
- http.Redirect(w, r, redirectURL, http.StatusFound)
- return
- }
- func (b *Builder) genBaseSessionClaim(id string) jwt.RegisteredClaims {
- return genBaseClaims(id, b.sessionMaxAge)
- }
- func (b *Builder) mustGetSessionToken(claims UserClaims) string {
- return mustSignClaims(claims, b.secret)
- }
- func (b *Builder) setAuthCookiesFromUserClaims(w http.ResponseWriter, claims *UserClaims, secureSalt string) {
- http.SetCookie(w, &http.Cookie{
- Name: b.authCookieName,
- Value: b.mustGetSessionToken(*claims),
- Path: b.cookieConfig.Path,
- Domain: b.cookieConfig.Domain,
- MaxAge: b.sessionMaxAge,
- Expires: time.Now().Add(time.Duration(b.sessionMaxAge) * time.Second),
- HttpOnly: true,
- Secure: true,
- SameSite: b.cookieConfig.SameSite,
- })
- if secureSalt != "" {
- http.SetCookie(w, &http.Cookie{
- Name: b.authSecureCookieName,
- Value: mustSignClaims(&claims.RegisteredClaims, b.secret+secureSalt),
- Path: b.cookieConfig.Path,
- Domain: b.cookieConfig.Domain,
- MaxAge: b.sessionMaxAge,
- Expires: time.Now().Add(time.Duration(b.sessionMaxAge) * time.Second),
- HttpOnly: true,
- Secure: true,
- SameSite: b.cookieConfig.SameSite,
- })
- }
- }
- func (b *Builder) cleanAuthCookies(w http.ResponseWriter) {
- http.SetCookie(w, &http.Cookie{
- Name: b.authCookieName,
- Value: "",
- Path: b.cookieConfig.Path,
- Domain: b.cookieConfig.Domain,
- MaxAge: -1,
- Expires: time.Unix(1, 0),
- HttpOnly: true,
- Secure: true,
- })
- http.SetCookie(w, &http.Cookie{
- Name: b.authSecureCookieName,
- Value: "",
- Path: b.cookieConfig.Path,
- Domain: b.cookieConfig.Domain,
- MaxAge: -1,
- Expires: time.Unix(1, 0),
- HttpOnly: true,
- Secure: true,
- })
- }
- func (b *Builder) setContinueURL(w http.ResponseWriter, r *http.Request) {
- continueURL := r.RequestURI
- if strings.Contains(continueURL, "?__execute_event__=") {
- continueURL = r.Referer()
- }
- ignore := false
- {
- ignoreURLs := map[string]struct{}{
- b.loginPageURL: {},
- b.resetPasswordPageURL: {},
- b.forgetPasswordPageURL: {},
- b.resetPasswordLinkSentPageURL: {},
- b.totpSetupPageURL: {},
- b.totpValidatePageURL: {},
- b.LogoutURL: {},
- }
- u, err := url.Parse(continueURL)
- if err != nil {
- ignore = true
- } else {
- if _, ok := ignoreURLs[u.Path]; ok {
- ignore = true
- }
- }
- }
- if ignore {
- return
- }
- http.SetCookie(w, &http.Cookie{
- Name: b.continueUrlCookieName,
- Value: continueURL,
- Path: b.cookieConfig.Path,
- Domain: b.cookieConfig.Domain,
- HttpOnly: true,
- })
- }
- func (b *Builder) getContinueURL(w http.ResponseWriter, r *http.Request) string {
- c, err := r.Cookie(b.continueUrlCookieName)
- if err != nil || c.Value == "" {
- return ""
- }
- http.SetCookie(w, &http.Cookie{
- Name: b.continueUrlCookieName,
- Value: "",
- MaxAge: -1,
- Expires: time.Unix(1, 0),
- Path: b.cookieConfig.Path,
- Domain: b.cookieConfig.Domain,
- HttpOnly: true,
- })
- return c.Value
- }
- func (b *Builder) setSecureCookiesByClaims(w http.ResponseWriter, user interface{}, claims UserClaims) (err error) {
- var secureSalt string
- if b.sessionSecureEnabled {
- if user.(SessionSecurer).GetSecure() == "" {
- err = user.(SessionSecurer).UpdateSecure(b.db, b.newUserObject(), objectID(user))
- if err != nil {
- return err
- }
- }
- secureSalt = user.(SessionSecurer).GetSecure()
- }
- b.setAuthCookiesFromUserClaims(w, &claims, secureSalt)
- return nil
- }
- func (b *Builder) consumeTOTPCode(r *http.Request, up UserPasser, passcode string) error {
- if !totp.Validate(passcode, up.GetTOTPSecret()) {
- return ErrWrongTOTPCode
- }
- lastCode, usedAt := up.GetLastUsedTOTPCode()
- if usedAt != nil && time.Now().Sub(*usedAt) > 90*time.Second {
- lastCode = ""
- }
- if passcode == lastCode {
- if b.afterTOTPCodeReusedHook != nil {
- if herr := b.afterTOTPCodeReusedHook(r, GetCurrentUser(r)); herr != nil {
- return herr
- }
- }
- return ErrTOTPCodeHasBeenUsed
- }
- if err := up.SetLastUsedTOTPCode(b.db, b.newUserObject(), passcode); err != nil {
- return err
- }
- return nil
- }
- // logout is for url "/logout/{provider}"
- func (b *Builder) logout(w http.ResponseWriter, r *http.Request) {
- err := gothic.Logout(w, r)
- if err != nil {
- //
- }
- b.cleanAuthCookies(w)
- if b.afterLogoutHook != nil {
- user := GetCurrentUser(r)
- if user != nil {
- if herr := b.afterLogoutHook(r, user); herr != nil {
- setNoticeOrPanic(w, herr)
- http.Redirect(w, r, b.loginPageURL, http.StatusFound)
- return
- }
- }
- }
- http.Redirect(w, r, b.loginPageURL, http.StatusFound)
- }
- // beginAuth is for url "/auth/{provider}"
- func (b *Builder) beginAuth(w http.ResponseWriter, r *http.Request) {
- gothic.BeginAuthHandler(w, r)
- }
- func (b *Builder) sendResetPasswordLink(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- failRedirectURL := b.forgetPasswordPageURL
- // check reCAPTCHA token
- if b.recaptchaEnabled {
- token := r.FormValue("token")
- if !recaptchaTokenCheck(b, token) {
- setFailCodeFlash(w, FailCodeIncorrectRecaptchaToken)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- }
- account := strings.TrimSpace(r.FormValue("account"))
- passcode := r.FormValue("otp")
- doTOTP := r.URL.Query().Get("totp") == "1"
- if doTOTP {
- failRedirectURL = MustSetQuery(failRedirectURL, "totp", "1")
- }
- if account == "" {
- setFailCodeFlash(w, FailCodeAccountIsRequired)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- u, err := b.userModel.(UserPasser).FindUser(b.db, b.newUserObject(), account)
- if err != nil {
- if errors.Is(err, gorm.ErrRecordNotFound) {
- http.Redirect(w, r, fmt.Sprintf("%s?a=%s", b.resetPasswordLinkSentPageURL, account), http.StatusFound)
- return
- }
- panic(err)
- }
- _, createdAt, _ := u.(UserPasser).GetResetPasswordToken()
- if createdAt != nil {
- v := 60 - int(time.Now().Sub(*createdAt).Seconds())
- if v > 0 {
- setSecondsToRedoFlash(w, v)
- setWrongForgetPasswordInputFlash(w, WrongForgetPasswordInputFlash{
- Account: account,
- })
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- }
- if u.(UserPasser).GetIsTOTPSetup() {
- if !doTOTP {
- setWrongForgetPasswordInputFlash(w, WrongForgetPasswordInputFlash{
- Account: account,
- })
- failRedirectURL = MustSetQuery(failRedirectURL, "totp", "1")
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- if err = b.consumeTOTPCode(r, u.(UserPasser), passcode); err != nil {
- var fc FailCode
- switch err {
- case ErrWrongTOTPCode:
- fc = FailCodeIncorrectTOTPCode
- case ErrTOTPCodeHasBeenUsed:
- fc = FailCodeTOTPCodeHasBeenUsed
- default:
- panic(err)
- }
- setNoticeOrFailCodeFlash(w, err, fc)
- setWrongForgetPasswordInputFlash(w, WrongForgetPasswordInputFlash{
- Account: account,
- TOTP: passcode,
- })
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- }
- token, err := u.(UserPasser).GenerateResetPasswordToken(b.db, b.newUserObject())
- if err != nil {
- panic(err)
- }
- scheme := "https"
- if r.TLS == nil {
- scheme = "http"
- }
- link := fmt.Sprintf("%s://%s%s?id=%s&token=%s", scheme, r.Host, b.resetPasswordPageURL, objectID(u), token)
- if doTOTP {
- link = MustSetQuery(link, "totp", "1")
- }
- if b.afterConfirmSendResetPasswordLinkHook != nil {
- if herr := b.afterConfirmSendResetPasswordLinkHook(r, u, link); herr != nil {
- setNoticeOrPanic(w, herr)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- }
- http.Redirect(w, r, fmt.Sprintf("%s?a=%s", b.resetPasswordLinkSentPageURL, account), http.StatusFound)
- return
- }
- func (b *Builder) doResetPassword(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- userID := r.FormValue("user_id")
- token := r.FormValue("token")
- passcode := r.FormValue("otp")
- doTOTP := r.URL.Query().Get("totp") == "1"
- failRedirectURL := fmt.Sprintf("%s?id=%s&token=%s", b.resetPasswordPageURL, userID, token)
- if doTOTP {
- failRedirectURL = MustSetQuery(failRedirectURL, "totp", "1")
- }
- if userID == "" {
- setFailCodeFlash(w, FailCodeUserNotFound)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- if token == "" {
- setFailCodeFlash(w, FailCodeInvalidToken)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- password := r.FormValue("password")
- confirmPassword := r.FormValue("confirm_password")
- if password == "" {
- setFailCodeFlash(w, FailCodePasswordCannotBeEmpty)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- if confirmPassword != password {
- setFailCodeFlash(w, FailCodePasswordNotMatch)
- setWrongResetPasswordInputFlash(w, WrongResetPasswordInputFlash{
- Password: password,
- ConfirmPassword: confirmPassword,
- })
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- u, err := b.findUserByID(userID)
- if err != nil {
- if err == ErrUserNotFound {
- setFailCodeFlash(w, FailCodeUserNotFound)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- panic(err)
- }
- storedToken, _, expired := u.(UserPasser).GetResetPasswordToken()
- if expired {
- setFailCodeFlash(w, FailCodeTokenExpired)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- if token != storedToken {
- setFailCodeFlash(w, FailCodeInvalidToken)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- if b.beforeSetPasswordHook != nil {
- if herr := b.beforeSetPasswordHook(r, u, password); herr != nil {
- setNoticeOrPanic(w, herr)
- setWrongResetPasswordInputFlash(w, WrongResetPasswordInputFlash{
- Password: password,
- ConfirmPassword: confirmPassword,
- })
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- }
- if u.(UserPasser).GetIsTOTPSetup() {
- if !doTOTP {
- setWrongResetPasswordInputFlash(w, WrongResetPasswordInputFlash{
- Password: password,
- ConfirmPassword: confirmPassword,
- })
- failRedirectURL = MustSetQuery(failRedirectURL, "totp", "1")
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- if err = b.consumeTOTPCode(r, u.(UserPasser), passcode); err != nil {
- var fc FailCode
- switch err {
- case ErrWrongTOTPCode:
- fc = FailCodeIncorrectTOTPCode
- case ErrTOTPCodeHasBeenUsed:
- fc = FailCodeTOTPCodeHasBeenUsed
- default:
- panic(err)
- }
- setFailCodeFlash(w, fc)
- setWrongResetPasswordInputFlash(w, WrongResetPasswordInputFlash{
- Password: password,
- ConfirmPassword: confirmPassword,
- TOTP: passcode,
- })
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- }
- err = u.(UserPasser).ConsumeResetPasswordToken(b.db, b.newUserObject())
- if err != nil {
- panic(err)
- }
- err = u.(UserPasser).SetPassword(b.db, b.newUserObject(), password)
- if err != nil {
- panic(err)
- }
- if b.afterResetPasswordHook != nil {
- if herr := b.afterResetPasswordHook(r, u); herr != nil {
- setNoticeOrPanic(w, herr)
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- return
- }
- }
- setInfoCodeFlash(w, InfoCodePasswordSuccessfullyReset)
- http.Redirect(w, r, b.loginPageURL, http.StatusFound)
- return
- }
- // NoticeError
- // ErrWrongPassword
- // ErrEmptyPassword
- // ErrPasswordNotMatch
- // ErrWrongTOTPCode
- // ErrTOTPCodeHasBeenUsed
- func (b *Builder) ChangePassword(
- r *http.Request,
- oldPassword string,
- password string,
- confirmPassword string,
- otp string,
- ) error {
- user := GetCurrentUser(r).(UserPasser)
- if !user.IsPasswordCorrect(oldPassword) {
- return ErrWrongPassword
- }
- if password == "" {
- return ErrEmptyPassword
- }
- if confirmPassword != password {
- return ErrPasswordNotMatch
- }
- if b.beforeSetPasswordHook != nil {
- if herr := b.beforeSetPasswordHook(r, user, password); herr != nil {
- return herr
- }
- }
- if b.totpEnabled {
- if err := b.consumeTOTPCode(r, user, otp); err != nil {
- return err
- }
- }
- err := user.SetPassword(b.db, b.newUserObject(), password)
- if err != nil {
- return err
- }
- if b.afterChangePasswordHook != nil {
- if herr := b.afterChangePasswordHook(r, user); herr != nil {
- return herr
- }
- }
- return nil
- }
- func (b *Builder) doFormChangePassword(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- oldPassword := r.FormValue("old_password")
- password := r.FormValue("password")
- confirmPassword := r.FormValue("confirm_password")
- otp := r.FormValue("otp")
- redirectURL := b.changePasswordPageURL
- err := b.ChangePassword(r, oldPassword, password, confirmPassword, otp)
- if err != nil {
- if ne, ok := err.(*NoticeError); ok {
- setNoticeFlash(w, ne)
- } else {
- var fc FailCode
- switch err {
- case ErrWrongPassword:
- fc = FailCodeIncorrectPassword
- case ErrEmptyPassword:
- fc = FailCodePasswordCannotBeEmpty
- case ErrPasswordNotMatch:
- fc = FailCodePasswordNotMatch
- case ErrWrongTOTPCode:
- fc = FailCodeIncorrectTOTPCode
- case ErrTOTPCodeHasBeenUsed:
- fc = FailCodeTOTPCodeHasBeenUsed
- default:
- panic(err)
- }
- setFailCodeFlash(w, fc)
- }
- setWrongChangePasswordInputFlash(w, WrongChangePasswordInputFlash{
- OldPassword: oldPassword,
- NewPassword: password,
- ConfirmPassword: confirmPassword,
- TOTP: otp,
- })
- http.Redirect(w, r, redirectURL, http.StatusFound)
- return
- }
- setInfoCodeFlash(w, InfoCodePasswordSuccessfullyChanged)
- http.Redirect(w, r, b.loginPageURL, http.StatusFound)
- }
- func (b *Builder) totpDo(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- var claims *UserClaims
- claims, err := parseUserClaimsFromCookie(r, b.authCookieName, b.secret)
- if err != nil {
- http.Redirect(w, r, b.LogoutURL, http.StatusFound)
- return
- }
- user, err := b.findUserByID(claims.UserID)
- if err != nil {
- if err == ErrUserNotFound {
- setFailCodeFlash(w, FailCodeUserNotFound)
- http.Redirect(w, r, b.LogoutURL, http.StatusFound)
- return
- }
- panic(err)
- }
- failRedirectURL := b.LogoutURL
- defer func() {
- if perr := recover(); perr != nil {
- panic(perr)
- }
- if err != nil {
- if b.afterFailedToLoginHook != nil {
- if herr := b.afterFailedToLoginHook(r, user, err); herr != nil {
- setNoticeOrPanic(w, herr)
- }
- }
- http.Redirect(w, r, failRedirectURL, http.StatusFound)
- }
- }()
- u := user.(UserPasser)
- otp := r.FormValue("otp")
- isTOTPSetup := u.GetIsTOTPSetup()
- if err = b.consumeTOTPCode(r, u, otp); err != nil {
- var fc FailCode
- switch err {
- case ErrWrongTOTPCode:
- fc = FailCodeIncorrectTOTPCode
- case ErrTOTPCodeHasBeenUsed:
- fc = FailCodeTOTPCodeHasBeenUsed
- default:
- panic(err)
- }
- setFailCodeFlash(w, fc)
- failRedirectURL = b.totpValidatePageURL
- if !isTOTPSetup {
- failRedirectURL = b.totpSetupPageURL
- }
- return
- }
- if !isTOTPSetup {
- if err = u.SetIsTOTPSetup(b.db, b.newUserObject(), true); err != nil {
- panic(err)
- }
- }
- claims.TOTPValidated = true
- if b.afterLoginHook != nil {
- setCookieForRequest(r, &http.Cookie{Name: b.authCookieName, Value: b.mustGetSessionToken(*claims)})
- if err = b.afterLoginHook(r, user); err != nil {
- setNoticeOrPanic(w, err)
- return
- }
- }
- err = b.setSecureCookiesByClaims(w, user, *claims)
- if err != nil {
- panic(err)
- }
- redirectURL := b.homePageURLFunc(r, user)
- if v := b.getContinueURL(w, r); v != "" {
- redirectURL = v
- }
- http.Redirect(w, r, redirectURL, http.StatusFound)
- }
- func (b *Builder) Mount(mux *http.ServeMux) {
- b.MountAPI(mux)
- // pages
- wb := web.New()
- mux.Handle(b.loginPageURL, b.i18nBuilder.EnsureLanguage(wb.Page(b.loginPageFunc)))
- if b.userPassEnabled {
- mux.Handle(b.resetPasswordPageURL, b.i18nBuilder.EnsureLanguage(wb.Page(b.resetPasswordPageFunc)))
- mux.Handle(b.changePasswordPageURL, b.i18nBuilder.EnsureLanguage(wb.Page(b.changePasswordPageFunc)))
- if !b.noForgetPasswordLink {
- mux.Handle(b.forgetPasswordPageURL, b.i18nBuilder.EnsureLanguage(wb.Page(b.forgetPasswordPageFunc)))
- mux.Handle(b.resetPasswordLinkSentPageURL, b.i18nBuilder.EnsureLanguage(wb.Page(b.resetPasswordLinkSentPageFunc)))
- }
- if b.totpEnabled {
- mux.Handle(b.totpSetupPageURL, b.i18nBuilder.EnsureLanguage(wb.Page(b.totpSetupPageFunc)))
- mux.Handle(b.totpValidatePageURL, b.i18nBuilder.EnsureLanguage(wb.Page(b.totpValidatePageFunc)))
- }
- }
- // assets
- assetsSubFS, err := fs.Sub(assetsFS, "assets")
- if err != nil {
- panic(err)
- }
- mux.Handle(assetsPathPrefix, http.StripPrefix(assetsPathPrefix, http.FileServer(http.FS(assetsSubFS))))
- }
- func (b *Builder) MountAPI(mux *http.ServeMux) {
- if len(b.secret) == 0 {
- panic("secret is empty")
- }
- if b.userModel != nil {
- if b.db == nil {
- panic("db is required")
- }
- }
- mux.HandleFunc(b.LogoutURL, b.logout)
- if b.userPassEnabled {
- mux.HandleFunc(b.passwordLoginURL, b.userpassLogin)
- mux.HandleFunc(b.resetPasswordURL, b.doResetPassword)
- mux.HandleFunc(b.changePasswordURL, b.doFormChangePassword)
- if !b.noForgetPasswordLink {
- mux.HandleFunc(b.sendResetPasswordLinkURL, b.sendResetPasswordLink)
- }
- if b.totpEnabled {
- mux.HandleFunc(b.validateTOTPURL, b.totpDo)
- }
- }
- if b.oauthEnabled {
- mux.HandleFunc(b.oauthBeginURL, b.beginAuth)
- mux.HandleFunc(b.oauthCallbackURL, b.completeUserAuthCallback)
- mux.HandleFunc(b.oauthCallbackCompleteURL, b.completeUserAuthCallbackComplete)
- }
- }
|