Skip to content
Open
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
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
113 changes: 69 additions & 44 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,14 +65,10 @@ func TestRegistration(t *testing.T) {
// sytest: POST /register can create a user
t.Run("POST /register can create a user", func(t *testing.T) {
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 @@ -80,14 +78,10 @@ func TestRegistration(t *testing.T) {
// sytest: POST /register downcases capitals in usernames
t.Run("POST /register downcases capitals in usernames", func(t *testing.T) {
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 @@ -98,15 +92,10 @@ func TestRegistration(t *testing.T) {
t.Run("POST /register returns the same device_id as that in the request", func(t *testing.T) {
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})
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 Down Expand Up @@ -134,14 +123,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 @@ -152,28 +140,22 @@ func TestRegistration(t *testing.T) {
})
t.Run("POST /register rejects if user already exists", func(t *testing.T) {
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"
}`)))
// Since servers validate the request body before handling auth, this returns 400
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'
Expand Down Expand Up @@ -308,6 +290,23 @@ 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) {
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 @@ -346,3 +345,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
}
Loading