mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-13 05:09:05 +02:00
Merge branch 'main' into actors/francis-addr
This commit is contained in:
@@ -36,6 +36,7 @@ type template[V any] struct {
|
||||
type templateData[V any] struct {
|
||||
AppName string
|
||||
LogoURL string
|
||||
AppURL string
|
||||
Data *V
|
||||
}
|
||||
|
||||
@@ -138,6 +139,7 @@ func send[V any](ctx context.Context, module *Module, dbConfig *appconfig.AppCon
|
||||
templateData := &templateData[V]{
|
||||
AppName: dbConfig.AppName.String(),
|
||||
LogoURL: common.EnvConfig.AppURL + "/api/application-images/email",
|
||||
AppURL: common.EnvConfig.AppURL,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestModuleSendsEveryEmailType(t *testing.T) {
|
||||
{
|
||||
name: "email verification",
|
||||
subject: "Verify your Pocket ID Test email address",
|
||||
bodyContains: []string{"EMAIL VERIFICATION", "Hello Test User", "https://id.example.test/verify-token"},
|
||||
bodyContains: []string{"VERIFY YOUR EMAIL ADDRESS", "Hello Test User", "https://id.example.test/verify-token"},
|
||||
send: func(ctx context.Context, config *appconfig.AppConfigModel) error {
|
||||
return module.SendEmailVerification(ctx, config, user.FullName(), userEmail, "https://id.example.test/verify-token")
|
||||
},
|
||||
|
||||
@@ -194,6 +194,22 @@ func writeErrorResponse(c *gin.Context, classified classifiedError, requestID st
|
||||
|
||||
func logRequestError(c *gin.Context, err error, classified classifiedError, requestID string) {
|
||||
if classified.status < http.StatusInternalServerError {
|
||||
cause := errors.Unwrap(err)
|
||||
if cause == nil {
|
||||
return
|
||||
}
|
||||
|
||||
slog.DebugContext(c.Request.Context(), "Request rejected",
|
||||
slog.String("error_code", string(classified.code)),
|
||||
slog.String("error_type", errorTypeName(err)),
|
||||
slog.String("cause_type", errorTypeName(cause)),
|
||||
slog.Int("http_status", classified.status),
|
||||
slog.String("request_id", requestID),
|
||||
slog.String("http_method", c.Request.Method),
|
||||
slog.String("http_path", c.Request.URL.Path),
|
||||
slog.Any("error", err),
|
||||
slog.Any("cause", cause),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -281,6 +281,7 @@ func (s *Service) VerifyLogin(ctx context.Context, dbConfig *appconfig.AppConfig
|
||||
Extensions: storedSession.Extensions,
|
||||
CredParams: storedSession.CredentialParams,
|
||||
}
|
||||
discardUnrequestedFalseAppIDOutput(session.Extensions, credentialAssertionData)
|
||||
|
||||
var user *model.User
|
||||
_, err := s.webAuthn.ValidateDiscoverableLogin(func(_, userHandle []byte) (gowebauthn.User, error) {
|
||||
@@ -535,6 +536,7 @@ func (s *Service) CreateReauthenticationTokenWithWebauthn(ctx context.Context, s
|
||||
Extensions: storedSession.Extensions,
|
||||
CredParams: storedSession.CredentialParams,
|
||||
}
|
||||
discardUnrequestedFalseAppIDOutput(session.Extensions, credentialAssertionData)
|
||||
|
||||
// Validate the credential assertion
|
||||
var user *model.User
|
||||
@@ -589,6 +591,21 @@ func classifyPasskeyError(err error, fallback func(error) *apperror.Error) *appe
|
||||
return fallback(err)
|
||||
}
|
||||
|
||||
func discardUnrequestedFalseAppIDOutput(session protocol.SessionExtensions, credential *protocol.ParsedCredentialAssertionData) {
|
||||
if credential == nil || credential.ClientExtensionResults.AppID == nil || *credential.ClientExtensionResults.AppID {
|
||||
return
|
||||
}
|
||||
|
||||
for _, requested := range session.Requested {
|
||||
if requested == protocol.ExtensionAppID {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Safari reports appid=false for security keys even when the relying party did not request the legacy extension
|
||||
credential.ClientExtensionResults.AppID = nil
|
||||
}
|
||||
|
||||
func (s *Service) ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error) {
|
||||
hashedToken := utils.CreateSha256Hash(token)
|
||||
var reauthToken ReauthenticationToken
|
||||
|
||||
@@ -263,6 +263,66 @@ func TestClassifyPasskeyErrorPreservesStructuredLookupFailure(t *testing.T) {
|
||||
require.ErrorIs(t, err, cause)
|
||||
}
|
||||
|
||||
func TestDiscardUnrequestedFalseAppIDOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requested []string
|
||||
appID bool
|
||||
extra map[string]any
|
||||
wantAppID *bool
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "unrequested false appid is discarded",
|
||||
appID: false,
|
||||
wantAppID: nil,
|
||||
},
|
||||
{
|
||||
name: "requested false appid is preserved",
|
||||
requested: []string{protocol.ExtensionAppID},
|
||||
appID: false,
|
||||
wantAppID: new(false),
|
||||
},
|
||||
{
|
||||
name: "unrequested true appid is rejected",
|
||||
appID: true,
|
||||
wantAppID: new(true),
|
||||
wantError: "appid",
|
||||
},
|
||||
{
|
||||
name: "other unsolicited output is rejected",
|
||||
appID: false,
|
||||
extra: map[string]any{"example": true},
|
||||
wantAppID: nil,
|
||||
wantError: "example",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
credential := &protocol.ParsedCredentialAssertionData{
|
||||
ParsedPublicKeyCredential: protocol.ParsedPublicKeyCredential{
|
||||
ClientExtensionResults: protocol.AuthenticationExtensionsClientOutputs{
|
||||
AppID: new(tc.appID),
|
||||
Extra: tc.extra,
|
||||
},
|
||||
},
|
||||
}
|
||||
session := protocol.SessionExtensions{Requested: tc.requested}
|
||||
|
||||
discardUnrequestedFalseAppIDOutput(session, credential)
|
||||
|
||||
assert.Equal(t, tc.wantAppID, credential.ClientExtensionResults.AppID)
|
||||
err := credential.ClientExtensionResults.Verify(session, protocol.AssertCeremony, protocol.UnsolicitedOutputPolicyReject)
|
||||
if tc.wantError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebAuthnManagementOperationsReturnSpecificNotFoundErrors(t *testing.T) {
|
||||
service, err := newService(Dependencies{
|
||||
DB: testutils.NewDatabaseForTest(t),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,7 @@
|
||||
{{define "root"}}{{.AppName}}
|
||||
|
||||
|
||||
API KEY EXPIRING SOON
|
||||
|
||||
Warning
|
||||
{{define "root"}}API KEY EXPIRING SOON
|
||||
|
||||
Hello {{.Data.Name}},
|
||||
This is a reminder that your API key {{.Data.ApiKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}.
|
||||
|
||||
Please generate a new API key if you need continued access.{{end}}
|
||||
Your API key {{.Data.ApiKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}. Anything that uses this key will stop working once it expires.
|
||||
|
||||
To keep access, create a new API key in your {{.AppName}} account settings before then.{{end}}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,10 @@
|
||||
{{define "root"}}{{.AppName}}
|
||||
|
||||
|
||||
EMAIL VERIFICATION
|
||||
{{define "root"}}VERIFY YOUR EMAIL ADDRESS
|
||||
|
||||
Hello {{.Data.UserFullName}},
|
||||
Click the button below to verify your email address for {{.AppName}}. This link will expire in 24 hours.
|
||||
|
||||
Click the button below to confirm the email address for your {{.AppName}} account. This link expires in 24 hours.
|
||||
|
||||
Verify {{.Data.VerificationLink}}{{end}}
|
||||
Verify email address {{.Data.VerificationLink}}
|
||||
|
||||
Or if you don't like clicking buttons, open this link:
|
||||
{{.Data.VerificationLink}}{{end}}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,26 +1,17 @@
|
||||
{{define "root"}}{{.AppName}}
|
||||
{{define "root"}}NEW SIGN-IN DETECTED
|
||||
|
||||
Your {{.AppName}} account was recently accessed from a new IP address or browser. If this was you, no further action is needed.
|
||||
|
||||
NEW SIGN-IN DETECTED
|
||||
|
||||
Warning
|
||||
|
||||
Your {{.AppName}} account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.
|
||||
|
||||
DETAILS
|
||||
|
||||
Approximate Location
|
||||
|
||||
Approximate location
|
||||
{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}
|
||||
|
||||
IP Address
|
||||
|
||||
IP address
|
||||
{{.Data.IPAddress}}
|
||||
|
||||
Device
|
||||
|
||||
{{.Data.Device}}
|
||||
|
||||
Sign-In Time
|
||||
Time
|
||||
{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}
|
||||
|
||||
{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}{{end}}
|
||||
If you don't recognize this activity, review the passkeys in your {{.AppName}} account settings and remove any you don't recognize.{{end}}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
|
||||
{{define "root"}}{{.AppName}}
|
||||
{{define "root"}}YOUR LOGIN CODE
|
||||
|
||||
Use the code below to sign in to {{.AppName}}. It expires in {{.Data.ExpirationString}}.
|
||||
|
||||
YOUR LOGIN CODE
|
||||
{{.Data.Code}}
|
||||
|
||||
Click the button below to sign in to {{.AppName}} with a login code.
|
||||
Or visit {{.Data.LoginLink}} and enter the code {{.Data.Code}}.
|
||||
Sign in {{.Data.LoginLinkWithCode}}
|
||||
|
||||
This code expires in {{.Data.ExpirationString}}.
|
||||
|
||||
Sign In {{.Data.LoginLinkWithCode}}{{end}}
|
||||
Or open {{.Data.LoginLink}} and enter the code manually.{{end}}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,3 @@
|
||||
{{define "root"}}{{.AppName}}
|
||||
|
||||
|
||||
TEST EMAIL
|
||||
{{define "root"}}TEST EMAIL
|
||||
|
||||
Your email setup is working correctly!{{end}}
|
||||
Reference in New Issue
Block a user