From af92b2f77cf458cb56a96128f22bc38fa33625d3 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 6 Mar 2023 18:21:34 +0100 Subject: [PATCH 1/2] Test the scope of a transaction IDs after using refresh token --- internal/client/client.go | 74 +++++++++++++++++++++++++++++++++++++++ tests/csapi/txnid_test.go | 49 ++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/internal/client/client.go b/internal/client/client.go index 9dde9516..76d66812 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -459,6 +459,80 @@ func (c *CSAPI) LoginUser(t *testing.T, localpart, password string, opts ...Logi return userID, accessToken, deviceID } +// LoginUserWithDeviceID will log in to a homeserver on an existing device +func (c *CSAPI) LoginUserWithDeviceID(t *testing.T, localpart, password, deviceID string) (userID, accessToken string) { + t.Helper() + reqBody := map[string]interface{}{ + "identifier": map[string]interface{}{ + "type": "m.id.user", + "user": localpart, + }, + "device_id": deviceID, + "password": password, + "type": "m.login.password", + } + res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody)) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("unable to read response body: %v", err) + } + + userID = gjson.GetBytes(body, "user_id").Str + accessToken = gjson.GetBytes(body, "access_token").Str + if gjson.GetBytes(body, "device_id").Str != deviceID { + t.Fatalf("device_id returned by login does not match the one requested") + } + return userID, accessToken +} + +// LoginUserWithRefreshToken will log in to a homeserver, with refresh token enabled, +// and create a new device on an existing user. +func (c *CSAPI) LoginUserWithRefreshToken(t *testing.T, localpart, password string) (userID, accessToken, refreshToken, deviceID string, expiresInMs int64) { + t.Helper() + reqBody := map[string]interface{}{ + "identifier": map[string]interface{}{ + "type": "m.id.user", + "user": localpart, + }, + "password": password, + "type": "m.login.password", + "refresh_token": true, + } + res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody)) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("unable to read response body: %v", err) + } + + userID = gjson.GetBytes(body, "user_id").Str + accessToken = gjson.GetBytes(body, "access_token").Str + deviceID = gjson.GetBytes(body, "device_id").Str + refreshToken = gjson.GetBytes(body, "refresh_token").Str + expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int() + return userID, accessToken, refreshToken, deviceID, expiresInMs +} + +// RefreshToken will consume a refresh token and return a new access token and refresh token. +func (c *CSAPI) ConsumeRefreshToken(t *testing.T, refreshToken string) (newAccessToken, newRefreshToken string, expiresInMs int64) { + t.Helper() + reqBody := map[string]interface{}{ + "refresh_token": refreshToken, + } + res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "refresh"}, WithJSONBody(t, reqBody)) + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("unable to read response body: %v", err) + } + + newAccessToken = gjson.GetBytes(body, "access_token").Str + newRefreshToken = gjson.GetBytes(body, "refresh_token").Str + expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int() + return newAccessToken, newRefreshToken, expiresInMs +} + // RegisterUser will register the user with given parameters and // return user ID, access token and device ID. It fails the test on network error. func (c *CSAPI) RegisterUser(t *testing.T, localpart, password string) (userID, accessToken, deviceID string) { diff --git a/tests/csapi/txnid_test.go b/tests/csapi/txnid_test.go index 01c055dd..8aca4006 100644 --- a/tests/csapi/txnid_test.go +++ b/tests/csapi/txnid_test.go @@ -213,3 +213,52 @@ func TestTxnIdempotency(t *testing.T) { must.NotEqualStr(t, eventID4, eventID3, "Expected eventID4 and eventID3 to be different, but they were not") } + +// TestTxnAfterRefresh tests that when a client refreshes its access token, +// it still gets back a transaction ID in the sync response. +func TestTxnAfterRefresh(t *testing.T) { + // Dendrite and Conduit don't support refresh tokens yet. + // Synapse has a broken implementation of refresh tokens: https://github.com/matrix-org/synapse/issues/15141 + runtime.SkipIf(t, runtime.Dendrite, runtime.Conduit, runtime.Synapse) + + deployment := Deploy(t, b.BlueprintCleanHS) + defer deployment.Destroy(t) + + deployment.RegisterUser(t, "hs1", "alice", "password", false) + + c := deployment.Client(t, "hs1", "") + + var refreshToken string + c.UserID, c.AccessToken, refreshToken, c.DeviceID, _ = c.LoginUserWithRefreshToken(t, "alice", "password") + + // Create a room where we can send events. + roomID := c.CreateRoom(t, map[string]interface{}{}) + + txnId := "abcdef" + // Let's send an event, and wait for it to appear in the sync. + eventID := c.SendEventUnsyncedWithTxnID(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "first", + }, + }, txnId) + + // When syncing, we should find the event and it should have a transaction ID. + token := c.MustSyncUntil(t, client.SyncReq{}, mustHaveTransactionIDForEvent(t, roomID, eventID, txnId)) + + // Now do the same, but refresh the token before syncing. + eventID = c.SendEventUnsyncedWithTxnID(t, roomID, b.Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "second", + }, + }, txnId) + + // Use the refresh token to get a new access token. + c.AccessToken, refreshToken, _ = c.ConsumeRefreshToken(t, refreshToken) + + // When syncing, we should find the event and it should also have a transaction ID. + c.MustSyncUntil(t, client.SyncReq{Since: token}, mustHaveTransactionIDForEvent(t, roomID, eventID, txnId)) +} From f0813b210dd091ea7b98ebec7dfca550fd332339 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 18 Apr 2023 18:06:51 +0100 Subject: [PATCH 2/2] Remove unused function --- internal/client/client.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index 76d66812..2accca90 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -459,33 +459,6 @@ func (c *CSAPI) LoginUser(t *testing.T, localpart, password string, opts ...Logi return userID, accessToken, deviceID } -// LoginUserWithDeviceID will log in to a homeserver on an existing device -func (c *CSAPI) LoginUserWithDeviceID(t *testing.T, localpart, password, deviceID string) (userID, accessToken string) { - t.Helper() - reqBody := map[string]interface{}{ - "identifier": map[string]interface{}{ - "type": "m.id.user", - "user": localpart, - }, - "device_id": deviceID, - "password": password, - "type": "m.login.password", - } - res := c.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody)) - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatalf("unable to read response body: %v", err) - } - - userID = gjson.GetBytes(body, "user_id").Str - accessToken = gjson.GetBytes(body, "access_token").Str - if gjson.GetBytes(body, "device_id").Str != deviceID { - t.Fatalf("device_id returned by login does not match the one requested") - } - return userID, accessToken -} - // LoginUserWithRefreshToken will log in to a homeserver, with refresh token enabled, // and create a new device on an existing user. func (c *CSAPI) LoginUserWithRefreshToken(t *testing.T, localpart, password string) (userID, accessToken, refreshToken, deviceID string, expiresInMs int64) {