Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
Expand Down Expand Up @@ -58,7 +58,7 @@ jobs:
timeout: 10m

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

Expand Down
29 changes: 23 additions & 6 deletions client/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,39 @@ func (c *CSAPI) ConsumeRefreshToken(t ct.TestLike, refreshToken string) (newAcce
}

// RegisterUser will register the user with given parameters and
// return user ID, access token and device ID. It fails the test on network error.
// return user ID, access token and device ID. It fails the test on network error,
// or if registration fails for another reason (e.g. server has non-dummy requirements).
func (c *CSAPI) RegisterUser(t ct.TestLike, localpart, password string) (userID, accessToken, deviceID string) {
t.Helper()
reqBody := map[string]interface{}{
"auth": map[string]string{
"type": "m.login.dummy",
},
reqBody := map[string]any{
"username": localpart,
"password": password,
}
res := c.MustDo(t, "POST", []string{"_matrix", "client", "v3", "register"}, WithJSONBody(t, reqBody))
// First request is expected to receive a UIA challenge
res := c.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, WithJSONBody(t, reqBody))
if res.StatusCode != 401 {
ct.Fatalf(t, "Expected 401 Unauthorized, got %d", res.StatusCode)
}

body, err := io.ReadAll(res.Body)
if err != nil {
ct.Fatalf(t, "unable to read response body: %v", err)
}
session := GetJSONFieldStr(t, body, "session")

// Now actually register the user
reqBody["auth"] = map[string]any{
"session": session,
"type": "m.login.dummy",
}
if session == "" {
delete(reqBody["auth"].(map[string]any), "session")
}
res = c.MustDo(t, "POST", []string{"_matrix", "client", "v3", "register"}, WithJSONBody(t, reqBody))
body, err = io.ReadAll(res.Body)
if err != nil {
ct.Fatalf(t, "unable to read response body: %v", err)
}

userID = GetJSONFieldStr(t, body, "user_id")
accessToken = GetJSONFieldStr(t, body, "access_token")
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/sirupsen/logrus v1.9.4
github.com/tidwall/gjson v1.19.0
github.com/tidwall/sjson v1.2.5
golang.org/x/crypto v0.53.0
golang.org/x/crypto v0.54.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
gonum.org/v1/plot v0.17.0
)
Expand Down Expand Up @@ -49,7 +49,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/image v0.41.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
)
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -109,39 +109,39 @@ go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLh
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
130 changes: 74 additions & 56 deletions tests/csapi/apidoc_register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"maps"
"net/http"
"net/url"
"testing"
Expand Down Expand Up @@ -63,17 +65,11 @@ func TestRegistration(t *testing.T) {
})
// sytest: POST /register can create a user
t.Run("POST /register can create a user", func(t *testing.T) {
// Venator: https://github.com/matrix-org/complement/issues/893
runtime.SkipIf(t, runtime.Venator)
t.Parallel()
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithRawBody(json.RawMessage(`{
"auth": {
"type": "m.login.dummy"
},
"username": "post-can-create-a-user",
"password": "sUp3rs3kr1t"
}`)))
reqBody, _ := startUIASession(t, unauthedClient, "post-can-create-a-user", "sUp3rs3kr1t", nil)
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyTypeEqual("access_token", gjson.String),
match.JSONKeyTypeEqual("user_id", gjson.String),
Expand All @@ -82,17 +78,11 @@ func TestRegistration(t *testing.T) {
})
// sytest: POST /register downcases capitals in usernames
t.Run("POST /register downcases capitals in usernames", func(t *testing.T) {
// Venator: https://github.com/matrix-org/complement/issues/893
runtime.SkipIf(t, runtime.Venator)
t.Parallel()
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithRawBody(json.RawMessage(`{
"auth": {
"type": "m.login.dummy"
},
"username": "user-UPPER",
"password": "sUp3rs3kr1t"
}`)))
reqBody, _ := startUIASession(t, unauthedClient, "user-UPPER", "sUp3rs3kr1t", nil)
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyTypeEqual("access_token", gjson.String),
match.JSONKeyEqual("user_id", "@user-upper:hs1"),
Expand All @@ -101,19 +91,12 @@ func TestRegistration(t *testing.T) {
})
// sytest: POST /register returns the same device_id as that in the request
t.Run("POST /register returns the same device_id as that in the request", func(t *testing.T) {
// Venator: https://github.com/matrix-org/complement/issues/893
runtime.SkipIf(t, runtime.Venator)
t.Parallel()
deviceID := "my_device_id"
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithRawBody(json.RawMessage(`{
"auth": {
"type": "m.login.dummy"
},
"username": "user-device",
"password": "sUp3rs3kr1t",
"device_id": "`+deviceID+`"
}`)))
reqBody, _ := startUIASession(t, unauthedClient, "user-device", deviceID, map[string]any{"device_id": deviceID})
Comment thread
FrenchGithubUser marked this conversation as resolved.
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 200,
JSON: []match.JSON{
match.JSONKeyTypeEqual("access_token", gjson.String),
match.JSONKeyEqual("device_id", deviceID),
Expand All @@ -122,8 +105,6 @@ func TestRegistration(t *testing.T) {
})
// sytest: POST /register rejects registration of usernames with '$q'
t.Run("POST /register rejects usernames with special characters", func(t *testing.T) {
// Venator: https://github.com/matrix-org/complement/issues/893
runtime.SkipIf(t, runtime.Venator)
t.Parallel()
specialChars := []string{
`!`,
Expand All @@ -143,14 +124,13 @@ func TestRegistration(t *testing.T) {
`'`,
}
for _, ch := range specialChars {
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"},
client.WithJSONBody(t, map[string]interface{}{
"auth": map[string]string{
"type": "m.login.dummy",
},
"username": "user-" + ch + "-reject-please",
"password": "sUp3rs3kr1t",
}))
reqBody := map[string]any{
"username": "user-" + ch + "-reject-please",
"password": "sUp3rs3kr1t",
}
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
// N.B. servers are expected to validate request bodies before handling UIA,
// so 400 is expected here, not 401.
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 400,
JSON: []match.JSON{
Expand All @@ -160,37 +140,30 @@ func TestRegistration(t *testing.T) {
}
})
t.Run("POST /register rejects if user already exists", func(t *testing.T) {
// Venator: https://github.com/matrix-org/complement/issues/893
runtime.SkipIf(t, runtime.Venator)
// Dendrite: auth is validated before input, meaning the second register request needs to start a fresh
// auth session. This conflicts with Synapse, which forbids a second session being started, as it
// validates the input before auth. Skip on Dendrite for now.
runtime.SkipIf(t, runtime.Dendrite)
t.Parallel()
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithRawBody(json.RawMessage(`{
"auth": {
"type": "m.login.dummy"
},
"username": "post-can-create-a-user-once",
"password": "sUp3rs3kr1t"
}`)))
reqBody, _ := startUIASession(t, unauthedClient, "post-can-create-a-user-once", "sUp3rs3kr1t", nil)
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
must.MatchResponse(t, res, match.HTTPResponse{
JSON: []match.JSON{
match.JSONKeyTypeEqual("access_token", gjson.String),
match.JSONKeyTypeEqual("user_id", gjson.String),
},
})
res = unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithRawBody(json.RawMessage(`{
"auth": {
"type": "m.login.dummy"
},
"username": "post-can-create-a-user-once",
"password": "anotherSuperSecret"
}`)))
delete(reqBody, "auth")
res = unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 400,
JSON: []match.JSON{
match.JSONKeyEqual("errcode", "M_USER_IN_USE"),
},
})
})
// sytest: POST /register allows registration of usernames with '$chr'
t.Run("POST /register allows registration of usernames with ", func(t *testing.T) {
// Venator: https://github.com/matrix-org/complement/issues/893
runtime.SkipIf(t, runtime.Venator)
testChars := []rune("q3._=-/")
for x := range testChars {
localpart := fmt.Sprintf("chrtestuser%s", string(testChars[x]))
Expand Down Expand Up @@ -321,6 +294,25 @@ func TestRegistration(t *testing.T) {
},
})
})
// Test that subsequent calls to /_matrix/client/v3/register after receiving a UIA
// challenge fail if the session is not provided.
t.Run("Registration without a session fails", func(t *testing.T) {
// Many implementations historically did not enforce this requirement strictly
runtime.SkipIf(t, runtime.Synapse, runtime.Dendrite, runtime.Conduit)
t.Parallel()
reqBody, session := startUIASession(t, unauthedClient, "auth-requires-session", "sUp3rs3kr1t", nil)
if session == "" {
t.Skip("Homeserver does not require a session for UIA")
}
delete(reqBody["auth"].(map[string]any), "session")
// Re-send the same request without the session.
// Since session is required if it is provided by the homeserver, this should
// return an error
res := unauthedClient.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 401,
})
})
})
}

Expand Down Expand Up @@ -359,3 +351,29 @@ func registerSharedSecret(t *testing.T, c *client.CSAPI, user, pass string, isAd
resp = c.Do(t, "POST", []string{"_synapse", "admin", "v1", "register"}, client.WithJSONBody(t, reqBody))
return resp
}

// startUIASession starts a UIA session and returns the updated request body,
// and associated session token, failing the test if the response is not a UIA challenge.
func startUIASession(t *testing.T, c *client.CSAPI, user, pass string, extra map[string]any) (map[string]any, string) {
reqBody := map[string]any{
"username": user,
"password": pass,
}
if extra != nil {
maps.Copy(reqBody, extra)
}
res := c.Do(t, "POST", []string{"_matrix", "client", "v3", "register"}, client.WithJSONBody(t, reqBody))
if res.StatusCode != 401 {
t.Fatalf("expected status code 401 (UIA challenge), got %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
session := client.GetJSONFieldStr(t, body, "session")
reqBody["auth"] = map[string]any{"session": session, "type": "m.login.dummy"}
if session == "" {
delete(reqBody["auth"].(map[string]any), "session")
}
return reqBody, session
}
2 changes: 1 addition & 1 deletion tests/csapi/power_levels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func TestPowerLevels(t *testing.T) {
func(body gjson.Result) error {
// This key should be missing for room v12+
if gomatrixserverlib.MustGetRoomVersion(defaultRoomVersion).PrivilegedCreators() {
must.MatchGJSON(t, body, match.JSONKeyMissing("users." + client.GjsonEscape(alice.UserID)))
must.MatchGJSON(t, body, match.JSONKeyMissing("users."+client.GjsonEscape(alice.UserID)))
return nil
} else {
userDefault := int(body.Get("users_default").Num)
Expand Down
Loading