-
-
Notifications
You must be signed in to change notification settings - Fork 889
feat(api): sign HTTP adapter notifications with phone-ID JWT #992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
41c41b9
2c930ed
4e29d1f
def44f1
7896ab5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
|
|
@@ -14,13 +15,17 @@ import ( | |
| "github.com/NdoleStudio/httpsms/pkg/telemetry" | ||
| "github.com/NdoleStudio/stacktrace" | ||
| "github.com/avast/retry-go/v5" | ||
| "github.com/golang-jwt/jwt/v5" | ||
| "github.com/google/uuid" | ||
| ) | ||
|
|
||
| const ( | ||
| maxNotificationResponseDiscardBytes = 4 * 1024 | ||
| notificationHTTPAttempts = 3 | ||
| notificationHTTPTimeout = 5 * time.Second | ||
| notificationHTTPRetryDelay = 250 * time.Millisecond | ||
| notificationJWTIssuer = "api.httpsms.com" | ||
| notificationJWTValidity = 10 * time.Minute | ||
| ) | ||
|
|
||
| // HTTPNotificationSender sends FCM-compatible gateway notifications to HTTPS adapters. | ||
|
|
@@ -62,6 +67,7 @@ func newHTTPNotificationSenderWithRetrier( | |
| func (sender *HTTPNotificationSender) Send( | ||
| ctx context.Context, | ||
| message *messaging.Message, | ||
| phoneID uuid.UUID, | ||
| ) (string, error) { | ||
| if message == nil { | ||
| return "", sender.notificationError("", "notification message is nil") | ||
|
|
@@ -78,8 +84,13 @@ func (sender *HTTPNotificationSender) Send( | |
| return "", sender.notificationError(hostname, "cannot encode notification") | ||
| } | ||
|
|
||
| authToken, err := sender.getAuthToken(endpoint, phoneID) | ||
| if err != nil { | ||
| return "", sender.notificationError(hostname, "cannot generate notification auth token") | ||
| } | ||
|
|
||
| err = sender.retrier.Do(func() error { | ||
| return sender.deliver(ctx, endpoint, body) | ||
| return sender.deliver(ctx, endpoint, body, authToken) | ||
| }) | ||
| if err == nil { | ||
| return "http/success", nil | ||
|
|
@@ -91,6 +102,27 @@ func (sender *HTTPNotificationSender) Send( | |
| return "", sender.notificationError(hostname, "notification request failed") | ||
| } | ||
|
|
||
| // getAuthToken generates a JWT bearer token for the HTTPS adapter, signed with the phone ID | ||
| // the same way webhook requests are signed with the webhook signing key. The phone ID is only | ||
| // used as the HMAC secret and is intentionally not embedded in any claim: the adapter already | ||
| // knows which phone ID to verify against from its own gateway registration, and putting the | ||
| // phone ID in a readable claim would let anyone who intercepts one token read the signing | ||
| // secret and forge further tokens. | ||
| func (sender *HTTPNotificationSender) getAuthToken(endpoint *url.URL, phoneID uuid.UUID) (string, error) { | ||
| audience := *endpoint | ||
| audience.User = nil | ||
|
|
||
| now := time.Now().UTC() | ||
| token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ | ||
| Audience: []string{audience.String()}, | ||
| ExpiresAt: jwt.NewNumericDate(now.Add(notificationJWTValidity)), | ||
| IssuedAt: jwt.NewNumericDate(now), | ||
| Issuer: notificationJWTIssuer, | ||
| NotBefore: jwt.NewNumericDate(now.Add(-notificationJWTValidity)), | ||
| }) | ||
| return token.SignedString([]byte(phoneID.String())) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The token uses How this was verified: The phone ID is embedded in the readable subject at line 118 and the identical value is used as the HMAC key at line 120.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in def44f1 / 7896ab5: removed the \sub\ claim entirely instead of just picking a different secret. The adapter already knows which phone ID to verify against from its own gateway registration (not from a token claim), so there's no need to embed the phone ID anywhere in the token — it's now used only as the HMAC signing secret and never appears in a readable claim. |
||
| } | ||
|
|
||
| func encodeHTTPNotificationPayload(message *messaging.Message) ([]byte, error) { | ||
| return json.Marshal(map[string]any{ | ||
| "message": message, | ||
|
|
@@ -101,6 +133,7 @@ func (sender *HTTPNotificationSender) deliver( | |
| ctx context.Context, | ||
| endpoint *url.URL, | ||
| body []byte, | ||
| authToken string, | ||
| ) error { | ||
| if err := ctx.Err(); err != nil { | ||
| return terminalNotificationRequestError{cause: err} | ||
|
|
@@ -109,7 +142,7 @@ func (sender *HTTPNotificationSender) deliver( | |
| attemptCtx, cancel := context.WithTimeout(ctx, sender.timeout) | ||
| defer cancel() | ||
|
|
||
| request, err := createHTTPNotificationRequest(attemptCtx, endpoint, body) | ||
| request, err := createHTTPNotificationRequest(attemptCtx, endpoint, body, authToken) | ||
| if err != nil { | ||
| return terminalNotificationRequestError{cause: err} | ||
| } | ||
|
|
@@ -125,6 +158,7 @@ func createHTTPNotificationRequest( | |
| ctx context.Context, | ||
| endpoint *url.URL, | ||
| body []byte, | ||
| authToken string, | ||
| ) (*http.Request, error) { | ||
| request, err := http.NewRequestWithContext( | ||
| ctx, | ||
|
|
@@ -136,6 +170,7 @@ func createHTTPNotificationRequest( | |
| return nil, err | ||
| } | ||
| request.Header.Set("Content-Type", "application/json") | ||
| request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) | ||
| return request, nil | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 7896ab5: updated the doc comment - phoneID identifies the receiving/target phone (the one being notified), not the sending phone.