Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions internal/api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,16 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error {
if config.Security.UpdatePasswordRequireCurrentPassword {
// user may be in a password reset flow, where they do not have a currentPassword
isRecoverySession := false
for _, claim := range session.AMRClaims {
// password recovery flows can be via otp or a magic link, check if the current session
// was created with one of those
if claim.GetAuthenticationMethod() == "otp" || claim.GetAuthenticationMethod() == "magiclink" {
isRecoverySession = true
break

// it is only a recovery session if it was recently created
if time.Now().Before(session.CreatedAt.Add(15*time.Minute)) {
for _, claim := range session.AMRClaims {
// password recovery flows can be via otp or a magic link, check if the current session
// was created with one of those
if claim.GetAuthenticationMethod() == "otp" || claim.GetAuthenticationMethod() == "magiclink" {
isRecoverySession = true
break
}
}
}
if !isRecoverySession {
Expand Down
39 changes: 39 additions & 0 deletions internal/api/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,27 @@ func (ts *UserTestSuite) TestUserUpdatePassword() {
notRecentlyLoggedIn.ID).Exec(),
)

// create a recovery session (OTP) created recently (within 15 minutes)
recentRecoverySession, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(recentRecoverySession))
require.NoError(ts.T(), models.AddClaimToSession(ts.API.db, recentRecoverySession.ID, models.OTP))
recentRecoverySession, err = models.FindSessionByID(ts.API.db, recentRecoverySession.ID, true)
require.NoError(ts.T(), err)

// create a recovery session (OTP) whose created_at is older than 15 minutes
staleRecoverySession, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(staleRecoverySession))
require.NoError(ts.T(), models.AddClaimToSession(ts.API.db, staleRecoverySession.ID, models.OTP))
require.NoError(ts.T(), ts.API.db.RawQuery(
"update "+staleRecoverySession.TableName()+" set created_at = ? where id = ?",
time.Now().Add(-20*time.Minute),
staleRecoverySession.ID).Exec(),
)
staleRecoverySession, err = models.FindSessionByID(ts.API.db, staleRecoverySession.ID, true)
require.NoError(ts.T(), err)

type expected struct {
code int
isAuthenticated bool
Expand Down Expand Up @@ -365,6 +386,24 @@ func (ts *UserTestSuite) TestUserUpdatePassword() {
sessionId: r.SessionId,
expected: expected{code: http.StatusBadRequest, isAuthenticated: false},
},
{
desc: "Current password not required for recent recovery session (OTP, within 15 minutes)",
newPassword: "newpassword123",
nonce: "",
requireReauthentication: false,
requireCurrentPassword: true,
sessionId: &recentRecoverySession.ID,
expected: expected{code: http.StatusOK, isAuthenticated: true},
},
{
desc: "Current password required for stale recovery session (OTP, older than 15 minutes)",
newPassword: "newpassword456",
nonce: "",
requireReauthentication: false,
requireCurrentPassword: true,
sessionId: &staleRecoverySession.ID,
expected: expected{code: http.StatusBadRequest, isAuthenticated: false},
},
}

for _, c := range cases {
Expand Down
Loading