mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-17 16:39:04 +02:00
feat: reduce one time access code length for better UX
This commit is contained in:
@@ -141,8 +141,8 @@ func (h *handler) exchangeToken(c *gin.Context) error {
|
||||
}
|
||||
|
||||
loginCode := c.Param("token")
|
||||
// reject invalid length login codes
|
||||
if len(loginCode) != 6 && len(loginCode) != 16 {
|
||||
// Reject values that cannot match either supported login code format
|
||||
if len(loginCode) != shortTokenLength && len(loginCode) != longTokenLength {
|
||||
return apperror.TokenInvalidOrExpired()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ import (
|
||||
|
||||
// authenticationMethodOneTimePassword identifies one-time password/code authentication
|
||||
// It must match the value emitted by the JWT service in the access token's "amr" claim
|
||||
const authenticationMethodOneTimePassword = "otp"
|
||||
const (
|
||||
authenticationMethodOneTimePassword = "otp"
|
||||
shortTokenLength = 6
|
||||
longTokenLength = 12
|
||||
)
|
||||
|
||||
// TokenStore is the minimal interface needed to persist a one-time access token in the actor state store.
|
||||
// It's satisfied by both *actor.Service (used by the running application) and *local.Host (used by CLI commands, which don't run the full actor host).
|
||||
@@ -263,10 +267,10 @@ func StoreToken(ctx context.Context, store TokenStore, userID string, ttl time.D
|
||||
|
||||
// generateToken generates the random token value (and optional device token) for a one-time access token.
|
||||
func generateToken(ttl time.Duration, withDeviceToken bool) (token string, deviceToken *string, err error) {
|
||||
// If expires at is less than 15 minutes, use a 6-character token instead of 16
|
||||
tokenLength := 16
|
||||
// Use the shorter format only for codes that expire within 15 minutes
|
||||
tokenLength := longTokenLength
|
||||
if ttl <= 15*time.Minute {
|
||||
tokenLength = 6
|
||||
tokenLength = shortTokenLength
|
||||
}
|
||||
|
||||
token, err = utils.GenerateRandomUnambiguousString(tokenLength)
|
||||
|
||||
@@ -71,6 +71,24 @@ func newServiceForTest(t *testing.T, db *gorm.DB) (*Service, *local.Host, *fakeA
|
||||
return svc, host, auditLog
|
||||
}
|
||||
|
||||
func TestGenerateTokenLength(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
ttl time.Duration
|
||||
wantLength int
|
||||
}{
|
||||
{name: "short-lived", ttl: 15 * time.Minute, wantLength: shortTokenLength},
|
||||
{name: "long-lived", ttl: time.Hour, wantLength: longTokenLength},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
token, _, err := generateToken(test.ttl, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, token, test.wantLength)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeTokenSuccess(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
svc, host, auditLog := newServiceForTest(t, db)
|
||||
|
||||
@@ -657,7 +657,7 @@ func (s *TestService) seedOneTimeAccessTokens(ctx context.Context) error {
|
||||
token string
|
||||
ttl time.Duration
|
||||
}{
|
||||
{token: "HPe6k6u1DRRVuAQV", ttl: time.Hour},
|
||||
{token: "HPe6k6u1DRRV", ttl: time.Hour},
|
||||
{token: "0ne-t1me-t0ken", ttl: time.Hour},
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,16 @@
|
||||
let isLoading = $state(false);
|
||||
let error: string | undefined = $state();
|
||||
let backHref = $state('/login/alternative');
|
||||
const shortCodeLength = 6;
|
||||
const longCodeLength = 12;
|
||||
|
||||
let longCodeRequested = $state(
|
||||
code.length > 6 || !$appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled
|
||||
code.length > shortCodeLength || !$appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled
|
||||
);
|
||||
let showLongCodeOption = $state($appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled);
|
||||
let codeComplete = $derived(longCodeRequested ? code.length === 16 : code.length === 6);
|
||||
let codeComplete = $derived(
|
||||
longCodeRequested ? code.length === longCodeLength : code.length === shortCodeLength
|
||||
);
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
@@ -94,7 +98,7 @@
|
||||
type="text"
|
||||
/>
|
||||
{:else}
|
||||
<InputOTP.Root maxlength={6} bind:value={code} autofocus>
|
||||
<InputOTP.Root maxlength={shortCodeLength} bind:value={code} autofocus>
|
||||
{#snippet children({ cells })}
|
||||
<InputOTP.Group>
|
||||
{#each cells as cell (cell)}
|
||||
|
||||
+2
-2
@@ -122,8 +122,8 @@ export const userGroups = {
|
||||
};
|
||||
|
||||
export const oneTimeAccessTokens = [
|
||||
{ token: 'HPe6k6u1DRRVuAQV', expired: false },
|
||||
{ token: 'YCGDtftvsvYW1Xd0', expired: true }
|
||||
{ token: 'HPe6k6u1DRRV', expired: false },
|
||||
{ token: 'YCGDtftvsvYW', expired: true }
|
||||
];
|
||||
|
||||
export const emailVerificationTokens = [
|
||||
|
||||
Reference in New Issue
Block a user