mirror of
https://github.com/hoernschen/dendrite.git
synced 2025-04-11 22:33:40 +00:00
refactor user interactive auth flow functions from register
Signed-off-by: mohit kumar singh <mohitkumarsingh907@gmail.com>
This commit is contained in:
parent
d4b24462d1
commit
50783a18ce
3 changed files with 332 additions and 240 deletions
|
@ -19,14 +19,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -68,31 +64,6 @@ func init() {
|
||||||
prometheus.MustRegister(amtRegUsers)
|
prometheus.MustRegister(amtRegUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sessionsDict keeps track of completed auth stages for each session.
|
|
||||||
type sessionsDict struct {
|
|
||||||
sessions map[string][]authtypes.LoginType
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCompletedStages returns the completed stages for a session.
|
|
||||||
func (d sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType {
|
|
||||||
if completedStages, ok := d.sessions[sessionID]; ok {
|
|
||||||
return completedStages
|
|
||||||
}
|
|
||||||
// Ensure that a empty slice is returned and not nil. See #399.
|
|
||||||
return make([]authtypes.LoginType, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddCompletedStage records that a session has completed an auth stage.
|
|
||||||
func (d *sessionsDict) AddCompletedStage(sessionID string, stage authtypes.LoginType) {
|
|
||||||
d.sessions[sessionID] = append(d.GetCompletedStages(sessionID), stage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSessionsDict() *sessionsDict {
|
|
||||||
return &sessionsDict{
|
|
||||||
sessions: make(map[string][]authtypes.LoginType),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// TODO: Remove old sessions. Need to do so on a session-specific timeout.
|
// TODO: Remove old sessions. Need to do so on a session-specific timeout.
|
||||||
// sessions stores the completed flow stages for all sessions. Referenced using their sessionID.
|
// sessions stores the completed flow stages for all sessions. Referenced using their sessionID.
|
||||||
|
@ -112,7 +83,7 @@ type registerRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
// user-interactive auth params
|
// user-interactive auth params
|
||||||
Auth authDict `json:"auth"`
|
UserInteractiveFlowRequest
|
||||||
|
|
||||||
InitialDisplayName *string `json:"initial_device_display_name"`
|
InitialDisplayName *string `json:"initial_device_display_name"`
|
||||||
|
|
||||||
|
@ -121,24 +92,6 @@ type registerRequest struct {
|
||||||
Type authtypes.LoginType `json:"type"`
|
Type authtypes.LoginType `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type authDict struct {
|
|
||||||
Type authtypes.LoginType `json:"type"`
|
|
||||||
Session string `json:"session"`
|
|
||||||
Mac gomatrixserverlib.HexString `json:"mac"`
|
|
||||||
|
|
||||||
// Recaptcha
|
|
||||||
Response string `json:"response"`
|
|
||||||
// TODO: Lots of custom keys depending on the type
|
|
||||||
}
|
|
||||||
|
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
|
||||||
type userInteractiveResponse struct {
|
|
||||||
Flows []authtypes.Flow `json:"flows"`
|
|
||||||
Completed []authtypes.LoginType `json:"completed"`
|
|
||||||
Params map[string]interface{} `json:"params"`
|
|
||||||
Session string `json:"session"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// legacyRegisterRequest represents the submitted registration request for v1 API.
|
// legacyRegisterRequest represents the submitted registration request for v1 API.
|
||||||
type legacyRegisterRequest struct {
|
type legacyRegisterRequest struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
@ -148,18 +101,6 @@ type legacyRegisterRequest struct {
|
||||||
Mac gomatrixserverlib.HexString `json:"mac"`
|
Mac gomatrixserverlib.HexString `json:"mac"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// newUserInteractiveResponse will return a struct to be sent back to the client
|
|
||||||
// during registration.
|
|
||||||
func newUserInteractiveResponse(
|
|
||||||
sessionID string,
|
|
||||||
fs []authtypes.Flow,
|
|
||||||
params map[string]interface{},
|
|
||||||
) userInteractiveResponse {
|
|
||||||
return userInteractiveResponse{
|
|
||||||
fs, sessions.GetCompletedStages(sessionID), params, sessionID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
||||||
type registerResponse struct {
|
type registerResponse struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
|
@ -231,68 +172,16 @@ func validatePassword(password string) *util.JSONResponse {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateRecaptcha returns an error response if the captcha response is invalid
|
// validateCredentials returns error if username or password is invalid
|
||||||
func validateRecaptcha(
|
func validateCredentials(username, password string) (jsonerr *util.JSONResponse) {
|
||||||
cfg *config.Dendrite,
|
jsonerr = validateUserName(username)
|
||||||
response string,
|
if jsonerr != nil {
|
||||||
clientip string,
|
return
|
||||||
) *util.JSONResponse {
|
|
||||||
if !cfg.Matrix.RecaptchaEnabled {
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("Captcha registration is disabled"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if response == "" {
|
jsonerr = validatePassword(password)
|
||||||
return &util.JSONResponse{
|
if jsonerr != nil {
|
||||||
Code: http.StatusBadRequest,
|
return
|
||||||
JSON: jsonerror.BadJSON("Captcha response is required"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a POST request to Google's API to check the captcha response
|
|
||||||
resp, err := http.PostForm(cfg.Matrix.RecaptchaSiteVerifyAPI,
|
|
||||||
url.Values{
|
|
||||||
"secret": {cfg.Matrix.RecaptchaPrivateKey},
|
|
||||||
"response": {response},
|
|
||||||
"remoteip": {clientip},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: jsonerror.BadJSON("Error in requesting validation of captcha response"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the request once we're finishing reading from it
|
|
||||||
defer resp.Body.Close() // nolint: errcheck
|
|
||||||
|
|
||||||
// Grab the body of the response from the captcha server
|
|
||||||
var r recaptchaResponse
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: jsonerror.BadJSON("Error in contacting captcha server" + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(body, &r)
|
|
||||||
if err != nil {
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: jsonerror.BadJSON("Error in unmarshaling captcha server's response: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we received a "success"
|
|
||||||
if !r.Success {
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -457,10 +346,7 @@ func Register(
|
||||||
// Squash username to all lowercase letters
|
// Squash username to all lowercase letters
|
||||||
r.Username = strings.ToLower(r.Username)
|
r.Username = strings.ToLower(r.Username)
|
||||||
|
|
||||||
if resErr = validateUsername(r.Username); resErr != nil {
|
if resErr = validateCredentials(r.Username, r.Password); resErr != nil {
|
||||||
return *resErr
|
|
||||||
}
|
|
||||||
if resErr = validatePassword(r.Password); resErr != nil {
|
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,17 +394,8 @@ func handleRegistrationFlow(
|
||||||
return util.MessageResponse(http.StatusForbidden, "Registration has been disabled")
|
return util.MessageResponse(http.StatusForbidden, "Registration has been disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jsonRes *util.JSONResponse
|
||||||
switch r.Auth.Type {
|
switch r.Auth.Type {
|
||||||
case authtypes.LoginTypeRecaptcha:
|
|
||||||
// Check given captcha response
|
|
||||||
resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
|
|
||||||
if resErr != nil {
|
|
||||||
return *resErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Recaptcha to the list of completed registration stages
|
|
||||||
sessions.AddCompletedStage(sessionID, authtypes.LoginTypeRecaptcha)
|
|
||||||
|
|
||||||
case authtypes.LoginTypeSharedSecret:
|
case authtypes.LoginTypeSharedSecret:
|
||||||
// Check shared secret against config
|
// Check shared secret against config
|
||||||
valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Auth.Mac)
|
valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Auth.Mac)
|
||||||
|
@ -546,50 +423,20 @@ func handleRegistrationFlow(
|
||||||
return completeRegistration(req.Context(), accountDB, deviceDB,
|
return completeRegistration(req.Context(), accountDB, deviceDB,
|
||||||
r.Username, "", appserviceID, r.InitialDisplayName)
|
r.Username, "", appserviceID, r.InitialDisplayName)
|
||||||
|
|
||||||
case authtypes.LoginTypeDummy:
|
|
||||||
// there is nothing to do
|
|
||||||
// Add Dummy to the list of completed registration stages
|
|
||||||
sessions.AddCompletedStage(sessionID, authtypes.LoginTypeDummy)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
jsonRes = HandleUserInteractiveFlow(req, r.UserInteractiveFlowRequest, sessionID, cfg, cfg.Derived.Registration)
|
||||||
Code: http.StatusNotImplemented,
|
if jsonRes == nil {
|
||||||
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
return completeRegistration(
|
||||||
|
req.Context(),
|
||||||
|
accountDB,
|
||||||
|
deviceDB,
|
||||||
|
r.Username,
|
||||||
|
r.Password,
|
||||||
|
"",
|
||||||
|
r.InitialDisplayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return *jsonRes
|
||||||
// Check if the user's registration flow has been completed successfully
|
|
||||||
// A response with current registration flow and remaining available methods
|
|
||||||
// will be returned if a flow has not been successfully completed yet
|
|
||||||
return checkAndCompleteFlow(sessions.GetCompletedStages(sessionID),
|
|
||||||
req, r, sessionID, cfg, accountDB, deviceDB)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndCompleteFlow checks if a given registration flow is completed given
|
|
||||||
// a set of allowed flows. If so, registration is completed, otherwise a
|
|
||||||
// response with
|
|
||||||
func checkAndCompleteFlow(
|
|
||||||
flow []authtypes.LoginType,
|
|
||||||
req *http.Request,
|
|
||||||
r registerRequest,
|
|
||||||
sessionID string,
|
|
||||||
cfg *config.Dendrite,
|
|
||||||
accountDB *accounts.Database,
|
|
||||||
deviceDB *devices.Database,
|
|
||||||
) util.JSONResponse {
|
|
||||||
if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) {
|
|
||||||
// This flow was completed, registration can continue
|
|
||||||
return completeRegistration(req.Context(), accountDB, deviceDB,
|
|
||||||
r.Username, r.Password, "", r.InitialDisplayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// There are still more stages to complete.
|
|
||||||
// Return the flows and those that have been completed.
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: newUserInteractiveResponse(sessionID,
|
|
||||||
cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LegacyRegister process register requests from the legacy v1 API
|
// LegacyRegister process register requests from the legacy v1 API
|
||||||
|
@ -779,58 +626,6 @@ func isValidMacLogin(
|
||||||
return hmac.Equal(givenMac, expectedMAC), nil
|
return hmac.Equal(givenMac, expectedMAC), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkFlows checks a single completed flow against another required one. If
|
|
||||||
// one contains at least all of the stages that the other does, checkFlows
|
|
||||||
// returns true.
|
|
||||||
func checkFlows(
|
|
||||||
completedStages []authtypes.LoginType,
|
|
||||||
requiredStages []authtypes.LoginType,
|
|
||||||
) bool {
|
|
||||||
// Create temporary slices so they originals will not be modified on sorting
|
|
||||||
completed := make([]authtypes.LoginType, len(completedStages))
|
|
||||||
required := make([]authtypes.LoginType, len(requiredStages))
|
|
||||||
copy(completed, completedStages)
|
|
||||||
copy(required, requiredStages)
|
|
||||||
|
|
||||||
// Sort the slices for simple comparison
|
|
||||||
sort.Slice(completed, func(i, j int) bool { return completed[i] < completed[j] })
|
|
||||||
sort.Slice(required, func(i, j int) bool { return required[i] < required[j] })
|
|
||||||
|
|
||||||
// Iterate through each slice, going to the next required slice only once
|
|
||||||
// we've found a match.
|
|
||||||
i, j := 0, 0
|
|
||||||
for j < len(required) {
|
|
||||||
// Exit if we've reached the end of our input without being able to
|
|
||||||
// match all of the required stages.
|
|
||||||
if i >= len(completed) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've found a stage we want, move on to the next required stage.
|
|
||||||
if completed[i] == required[j] {
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkFlowCompleted checks if a registration flow complies with any allowed flow
|
|
||||||
// dictated by the server. Order of stages does not matter. A user may complete
|
|
||||||
// extra stages as long as the required stages of at least one flow is met.
|
|
||||||
func checkFlowCompleted(
|
|
||||||
flow []authtypes.LoginType,
|
|
||||||
allowedFlows []authtypes.Flow,
|
|
||||||
) bool {
|
|
||||||
// Iterate through possible flows to check whether any have been fully completed.
|
|
||||||
for _, allowedFlow := range allowedFlows {
|
|
||||||
if checkFlows(flow, allowedFlow.Stages) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type availableResponse struct {
|
type availableResponse struct {
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,293 @@
|
||||||
|
// Copyright 2017 Vector Creations Ltd
|
||||||
|
// Copyright 2017 New Vector Ltd
|
||||||
|
//
|
||||||
|
// 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 routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/common/config"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authDict struct {
|
||||||
|
Type authtypes.LoginType `json:"type"`
|
||||||
|
Session string `json:"session"`
|
||||||
|
Mac gomatrixserverlib.HexString `json:"mac"`
|
||||||
|
|
||||||
|
// Recaptcha
|
||||||
|
Response string `json:"response"`
|
||||||
|
// TODO: Lots of custom keys depending on the type
|
||||||
|
}
|
||||||
|
|
||||||
|
// sessionsDict keeps track of completed auth stages for each session.
|
||||||
|
type sessionsDict struct {
|
||||||
|
sessions map[string][]authtypes.LoginType
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompletedStages returns the completed stages for a session.
|
||||||
|
func (d sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType {
|
||||||
|
if completedStages, ok := d.sessions[sessionID]; ok {
|
||||||
|
return completedStages
|
||||||
|
}
|
||||||
|
// Ensure that a empty slice is returned and not nil. See #399.
|
||||||
|
return make([]authtypes.LoginType, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AAddCompletedStage records that a session has completed an auth stage.
|
||||||
|
func (d *sessionsDict) AddCompletedStage(sessionID string, stage authtypes.LoginType) {
|
||||||
|
d.sessions[sessionID] = append(d.GetCompletedStages(sessionID), stage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSessionsDict() *sessionsDict {
|
||||||
|
return &sessionsDict{
|
||||||
|
sessions: make(map[string][]authtypes.LoginType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInteractiveFlowRequest is a generic flowRequest type for any auth call to UIAA handler
|
||||||
|
type UserInteractiveFlowRequest struct {
|
||||||
|
// user-interactive auth params
|
||||||
|
Auth authDict `json:"auth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInteractiveResponse is response returned by UIAA flow handler to the client
|
||||||
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
||||||
|
type UserInteractiveResponse struct {
|
||||||
|
Flows []authtypes.Flow `json:"flows"`
|
||||||
|
Completed []authtypes.LoginType `json:"completed"`
|
||||||
|
Params map[string]interface{} `json:"params"`
|
||||||
|
Session string `json:"session"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// newUserInteractiveResponse will return a struct to be sent back to the client
|
||||||
|
func newUserInteractiveResponse(
|
||||||
|
sessionID string,
|
||||||
|
fs []authtypes.Flow,
|
||||||
|
params map[string]interface{},
|
||||||
|
) UserInteractiveResponse {
|
||||||
|
return UserInteractiveResponse{
|
||||||
|
fs, sessions.GetCompletedStages(sessionID), params, sessionID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recaptchaResponse represents the HTTP response from a Google Recaptcha server
|
||||||
|
type recaptchaResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ChallengeTS time.Time `json:"challenge_ts"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
ErrorCodes []string `json:"error-codes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRecaptcha returns an error response if the captcha response is invalid
|
||||||
|
func validateRecaptcha(
|
||||||
|
cfg *config.Dendrite,
|
||||||
|
response string,
|
||||||
|
clientip string,
|
||||||
|
) *util.JSONResponse {
|
||||||
|
if !cfg.Matrix.RecaptchaEnabled {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("Captcha registration is disabled"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if response == "" {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("Captcha response is required"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a POST request to Google's API to check the captcha response
|
||||||
|
resp, err := http.PostForm(cfg.Matrix.RecaptchaSiteVerifyAPI,
|
||||||
|
url.Values{
|
||||||
|
"secret": {cfg.Matrix.RecaptchaPrivateKey},
|
||||||
|
"response": {response},
|
||||||
|
"remoteip": {clientip},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: jsonerror.BadJSON("Error in requesting validation of captcha response"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the request once we're finishing reading from it
|
||||||
|
defer resp.Body.Close() // nolint: errcheck
|
||||||
|
|
||||||
|
// Grab the body of the response from the captcha server
|
||||||
|
var r recaptchaResponse
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: jsonerror.BadJSON("Error in contacting captcha server" + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: jsonerror.BadJSON("Error in unmarshaling captcha server's response: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we received a "success"
|
||||||
|
if !r.Success {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleUserInteractiveFlow will direct and complete UIAA flow stages that the client has requested.
|
||||||
|
// It accepts a pointer to http request, an interface of type UserInteractiveFlowRequest, config,
|
||||||
|
// sessionID and a list of required stages to complete the flow as config.UserInteractiveAuthConfig
|
||||||
|
// and returns UserInteractiveResponse as specified in
|
||||||
|
// https://matrix.org/docs/spec/client_server/r0.3.0.html#user-interactive-authentication-api
|
||||||
|
// The function returns nil if authenticated successfully.
|
||||||
|
func HandleUserInteractiveFlow(
|
||||||
|
req *http.Request,
|
||||||
|
r UserInteractiveFlowRequest,
|
||||||
|
sessionID string,
|
||||||
|
cfg *config.Dendrite,
|
||||||
|
res config.UserInteractiveAuthConfig,
|
||||||
|
) *util.JSONResponse {
|
||||||
|
|
||||||
|
// TODO: email / msisdn auth types.
|
||||||
|
|
||||||
|
switch r.Auth.Type {
|
||||||
|
case "":
|
||||||
|
// If no auth type is specified by the client, send back the list of available flows
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: newUserInteractiveResponse(sessionID,
|
||||||
|
res.Flows, res.Params),
|
||||||
|
}
|
||||||
|
|
||||||
|
case authtypes.LoginTypeRecaptcha:
|
||||||
|
// Check given captcha response
|
||||||
|
resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
|
||||||
|
if resErr != nil {
|
||||||
|
return resErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Recaptcha to the list of completed stages
|
||||||
|
sessions.AddCompletedStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||||
|
|
||||||
|
case authtypes.LoginTypeDummy:
|
||||||
|
// there is nothing to do
|
||||||
|
// Add Dummy to the list of completed stages
|
||||||
|
sessions.AddCompletedStage(sessionID, authtypes.LoginTypeDummy)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusNotImplemented,
|
||||||
|
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user's flow has been completed successfully
|
||||||
|
// A response with current flow and remaining available methods
|
||||||
|
// will be returned if a flow has not been successfully completed yet
|
||||||
|
return checkAndCompleteFlow(sessions.GetCompletedStages(sessionID), sessionID, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndCompleteFlow checks if a given flow is completed given
|
||||||
|
// a set of allowed flows. If so, task is completed, otherwise a
|
||||||
|
// response with more stages to complete is returned.
|
||||||
|
func checkAndCompleteFlow(
|
||||||
|
flow []authtypes.LoginType,
|
||||||
|
sessionID string,
|
||||||
|
allowedFlows config.UserInteractiveAuthConfig,
|
||||||
|
) *util.JSONResponse {
|
||||||
|
if checkFlowCompleted(flow, allowedFlows.Flows) {
|
||||||
|
// This flow was completed, task can continue
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are still more stages to complete.
|
||||||
|
// Return the flows and those that have been completed.
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: newUserInteractiveResponse(sessionID,
|
||||||
|
allowedFlows.Flows, allowedFlows.Params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFlows checks a single completed flow against another required one. If
|
||||||
|
// one contains at least all of the stages that the other does, checkFlows
|
||||||
|
// returns true.
|
||||||
|
func checkFlows(
|
||||||
|
completedStages []authtypes.LoginType,
|
||||||
|
requiredStages []authtypes.LoginType,
|
||||||
|
) bool {
|
||||||
|
// Create temporary slices so they originals will not be modified on sorting
|
||||||
|
completed := make([]authtypes.LoginType, len(completedStages))
|
||||||
|
required := make([]authtypes.LoginType, len(requiredStages))
|
||||||
|
copy(completed, completedStages)
|
||||||
|
copy(required, requiredStages)
|
||||||
|
|
||||||
|
// Sort the slices for simple comparison
|
||||||
|
sort.Slice(completed, func(i, j int) bool { return completed[i] < completed[j] })
|
||||||
|
sort.Slice(required, func(i, j int) bool { return required[i] < required[j] })
|
||||||
|
|
||||||
|
// Iterate through each slice, going to the next required slice only once
|
||||||
|
// we've found a match.
|
||||||
|
i, j := 0, 0
|
||||||
|
for j < len(required) {
|
||||||
|
// Exit if we've reached the end of our input without being able to
|
||||||
|
// match all of the required stages.
|
||||||
|
if i >= len(completed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've found a stage we want, move on to the next required stage.
|
||||||
|
if completed[i] == required[j] {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFlowCompleted checks if a flow complies with any allowed flow
|
||||||
|
// dictated by the server. Order of stages does not matter. A user may complete
|
||||||
|
// extra stages as long as the required stages of at least one flow is met.
|
||||||
|
func checkFlowCompleted(
|
||||||
|
flow []authtypes.LoginType,
|
||||||
|
allowedFlows []authtypes.Flow,
|
||||||
|
) bool {
|
||||||
|
// Iterate through possible flows to check whether any have been fully completed.
|
||||||
|
for _, allowedFlow := range allowedFlows {
|
||||||
|
if checkFlows(flow, allowedFlow.Stages) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -222,19 +222,8 @@ type Dendrite struct {
|
||||||
|
|
||||||
// Any information derived from the configuration options for later use.
|
// Any information derived from the configuration options for later use.
|
||||||
Derived struct {
|
Derived struct {
|
||||||
Registration struct {
|
Registration UserInteractiveAuthConfig
|
||||||
// Flows is a slice of flows, which represent one possible way that the client can authenticate a request.
|
// Application Services parsed from their config files
|
||||||
// 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 []authtypes.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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Application services parsed from their config files
|
|
||||||
// The paths of which were given above in the main config file
|
// The paths of which were given above in the main config file
|
||||||
ApplicationServices []ApplicationService
|
ApplicationServices []ApplicationService
|
||||||
|
|
||||||
|
@ -252,6 +241,21 @@ type Dendrite struct {
|
||||||
} `yaml:"-"`
|
} `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserInteractiveAuthConfig , configuration for user interactive Flow API. Contains sets of auth flows,
|
||||||
|
// possible ways in which a client can authenticate via UIAA and params returned to the client for the same.
|
||||||
|
// http://matrix.org/docs/spec/HEAD/client_server/r0.3.0.html#user-interactive-authentication-api
|
||||||
|
type UserInteractiveAuthConfig 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 []authtypes.Flow `json:"flows"`
|
||||||
|
|
||||||
|
// Params that need to be returned to the client
|
||||||
|
// in order to complete auth stages.
|
||||||
|
Params map[string]interface{} `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
// A Path on the filesystem.
|
// A Path on the filesystem.
|
||||||
type Path string
|
type Path string
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue