Merge branch 'main' into actors/francis-addr

This commit is contained in:
Alessandro (Ale) Segala
2026-09-20 10:14:39 -07:00
committed by GitHub
41 changed files with 1552 additions and 762 deletions
+2
View File
@@ -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,
}
+1 -1
View File
@@ -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
}
+17
View File
@@ -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
+60
View File
@@ -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}}