Skip to content
This repository has been archived by the owner on Nov 25, 2024. It is now read-only.

Implement registering with an email support #1837

Closed
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
2 changes: 1 addition & 1 deletion clientapi/auth/authtypes/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ package authtypes
// Flow represents one possible way that the client can authenticate a request.
// https://matrix.org/docs/spec/client_server/r0.3.0.html#user-interactive-authentication-api
type Flow struct {
Stages []LoginType `json:"stages"`
Stages []LoginType `json:"stages" yaml:"stages"`
}
27 changes: 27 additions & 0 deletions clientapi/auth/authtypes/interactive_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright Piotr Kozimor <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package authtypes

type InteractiveAuth struct {
// Flows is a slice of flows, which represent one possible way that the client can authenticate a request.
// http://matrix.org/docs/spec/HEAD/client_server/r0.3.0.html#user-interactive-authentication-api
// As long as the generated flows only rely on config file options,
// we can generate them on startup and store them until needed
Flows []Flow `json:"flows"`

// Params that need to be returned to the client during
// registration in order to complete registration stages.
Params map[string]interface{} `json:"params"`
}
1 change: 1 addition & 0 deletions clientapi/auth/authtypes/logintypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ const (
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
LoginTypeRecaptcha = "m.login.recaptcha"
LoginTypeApplicationService = "m.login.application_service"
LoginTypeEmailIdentity = "m.login.email.identity"
)
99 changes: 97 additions & 2 deletions clientapi/routing/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/threepid"
"github.com/matrix-org/dendrite/clientapi/userutil"
userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/dendrite/userapi/storage/accounts"
Expand Down Expand Up @@ -150,6 +151,8 @@ type authDict struct {

// Recaptcha
Response string `json:"response"`
// m.login.email.identity and m.login.msisdn
ThreePidCreds *threepid.Credentials `json:"threepidCreds"`
// TODO: Lots of custom keys depending on the type
}

Expand Down Expand Up @@ -310,6 +313,80 @@ func validateRecaptcha(
return nil
}

func validateEmailIdentity(
ctx context.Context,
cred *threepid.Credentials,
cfg *config.ClientAPI,
) *util.JSONResponse {
if err := isTrusted(cred.IDServer, cfg); err != nil {
return &util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.NotTrusted(cred.IDServer),
}
}
util.GetLogger(ctx).Infof("conecting to identity server: %s", cred.IDServer)
url := fmt.Sprintf(
"https://%s/_matrix/identity/api/v1/3pid/getValidated3pid",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dendrite isn't my project, but:

this endpoint is deprecated by MSC2713 and it's likely that sydent will soon drop support for it.

In general, homeservers should not be delegating responsibility for email address validation to identity servers, since it allows a compromised ID server to be used to take over homeserver accounts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for comment. I think I have misunderstood concept of ID server. It makes sense that homeserver sends email on its own. So ID server would be user after registration to publish association, so that other may find user by email, right? I suppose also that auth stage for login should be also done without ID server. I am right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's right.

cred.IDServer)
req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
if err != nil {
return &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.InternalServerError(),
}
}
q := req.URL.Query()
q.Add("client_secret", cred.Secret)
q.Add("sid", cred.SID)
req.URL.RawQuery = q.Encode()
req.Header.Add("Authorization", "Bearer swordfish")
resp, err := cfg.Derived.HttpClient.Do(req)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("failed conecting to identity server")
return &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("failed conecting to identity server"),
}
}
defer func() {
err = resp.Body.Close()
if err != nil {
util.GetLogger(ctx).WithError(err).Error("validateEmailIdentity: unable to close response body")
}
}()
switch resp.StatusCode {
case 404:
return &util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("provided sid or client_secret not found on identity server"),
}
case 400:
return &util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("session has not been validated"),
}
case 200:
return nil
default:
return &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.InternalServerError(),
}
}
}

// isTrusted checks if a given identity server is part of the list of trusted
// identity servers in the configuration file.
// Returns an error if the server isn't trusted.
func isTrusted(idServer string, cfg *config.ClientAPI) error {
for _, server := range cfg.Matrix.TrustedIDServers {
if idServer == server {
return nil
}
}
return threepid.ErrNotTrusted
}

// UserIDIsWithinApplicationServiceNamespace checks to see if a given userID
// falls within any of the namespaces of a given Application Service. If no
// Application Service is given, it will check to see if it matches any
Expand Down Expand Up @@ -659,6 +736,24 @@ func handleRegistrationFlow(
// Add Dummy to the list of completed registration stages
AddCompletedSessionStage(sessionID, authtypes.LoginTypeDummy)

case authtypes.LoginTypeEmailIdentity:
if r.Auth.ThreePidCreds == nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("threepidCreds not found in auth field"),
}
}
if err := r.Auth.ThreePidCreds.Validate(); err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: err,
}
}
if err := validateEmailIdentity(req.Context(), r.Auth.ThreePidCreds, cfg); err != nil {
return *err
}
AddCompletedSessionStage(sessionID, authtypes.LoginTypeEmailIdentity)

case "":
// An empty auth type means that we want to fetch the available
// flows. It can also mean that we want to register as an appservice
Expand Down Expand Up @@ -731,7 +826,7 @@ func checkAndCompleteFlow(
cfg *config.ClientAPI,
userAPI userapi.UserInternalAPI,
) util.JSONResponse {
if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) {
if checkFlowCompleted(flow, cfg.Registration.Flows) {
// This flow was completed, registration can continue
return completeRegistration(
req.Context(), userAPI, r.Username, r.Password, "", req.RemoteAddr, req.UserAgent(),
Expand All @@ -744,7 +839,7 @@ func checkAndCompleteFlow(
return util.JSONResponse{
Code: http.StatusUnauthorized,
JSON: newUserInteractiveResponse(sessionID,
cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params),
cfg.Registration.Flows, cfg.Registration.Params),
}
}

Expand Down
53 changes: 39 additions & 14 deletions clientapi/threepid/threepid.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
package threepid

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/util"
)

// EmailAssociationRequest represents the request defined at https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register-email-requesttoken
Expand All @@ -48,6 +50,19 @@ type Credentials struct {
Secret string `json:"client_secret"`
}

func (c *Credentials) Validate() *jsonerror.MatrixError {
if c.SID == "" {
return jsonerror.BadJSON("sid field in threepidCreds is required")
}
if c.Secret == "" {
return jsonerror.BadJSON("client_secret in threepidCreds is required")
}
if c.IDServer == "" {
return jsonerror.BadJSON("id_server in threepidCreds is required")
}
return nil
}

// CreateSession creates a session on an identity server.
// Returns the session's ID.
// Returns an error if there was a problem sending the request or decoding the
Expand All @@ -62,22 +77,28 @@ func CreateSession(
// Create a session on the ID server
postURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/validate/email/requestToken", req.IDServer)

data := url.Values{}
data.Add("client_secret", req.Secret)
data.Add("email", req.Email)
data.Add("send_attempt", strconv.Itoa(req.SendAttempt))

request, err := http.NewRequest(http.MethodPost, postURL, strings.NewReader(data.Encode()))
b := bytes.Buffer{}
enc := json.NewEncoder(&b)
err := enc.Encode(req)
if err != nil {
return "", err
}
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
request, err := http.NewRequest(http.MethodPost, postURL, &b)
if err != nil {
return "", err
}
request.Header.Add("Content-Type", "application/json")

client := http.Client{}
resp, err := client.Do(request.WithContext(ctx))
resp, err := cfg.Derived.HttpClient.Do(request.WithContext(ctx))
if err != nil {
return "", err
}
defer func() {
err = resp.Body.Close()
if err != nil {
util.GetLogger(ctx).WithError(err).Error("CreateSession: unable to close response body")
}
}()

// Error if the status isn't OK
if resp.StatusCode != http.StatusOK {
Expand Down Expand Up @@ -112,11 +133,16 @@ func CheckAssociation(
if err != nil {
return false, "", "", err
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
resp, err := cfg.Derived.HttpClient.Do(req.WithContext(ctx))
if err != nil {
return false, "", "", err
}

defer func() {
err = resp.Body.Close()
if err != nil {
util.GetLogger(ctx).WithError(err).Error("CheckAssociation: unable to close response body")
}
}()
var respBody struct {
Medium string `json:"medium"`
ValidatedAt int64 `json:"validated_at"`
Expand Down Expand Up @@ -160,8 +186,7 @@ func PublishAssociation(creds Credentials, userID string, cfg *config.ClientAPI)
}
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")

client := http.Client{}
resp, err := client.Do(request)
resp, err := cfg.Derived.HttpClient.Do(request)
if err != nil {
return err
}
Expand Down
9 changes: 8 additions & 1 deletion dendrite-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,14 @@ client_api:
connect: http://localhost:7771
external_api:
listen: http://[::]:8071

registration:
flows:
- stages:
- m.login.email.identity
login:
flows:
- stages:
- m.login.password
# Prevents new users from being able to register on this homeserver, except when
# using the registration shared secret below.
registration_disabled: false
Expand Down
Loading