mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-16 11:23:11 -06:00
286 lines
9 KiB
Go
286 lines
9 KiB
Go
// Copyright 2019 Vector Creations Ltd
|
|
// Copyright 2019 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
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// getCompletedStages returns the completed stages for a session.
|
|
func (d *sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType {
|
|
d.Lock()
|
|
defer d.Unlock()
|
|
|
|
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)
|
|
}
|
|
|
|
// addCompletedSessionStage records that a session has completed an auth stage.
|
|
func AddCompletedSessionStage(sessionID string, stage authtypes.LoginType) {
|
|
sessions.Lock()
|
|
defer sessions.Unlock()
|
|
|
|
for _, completedStage := range sessions.sessions[sessionID] {
|
|
if completedStage == stage {
|
|
return
|
|
}
|
|
}
|
|
sessions.sessions[sessionID] = append(sessions.sessions[sessionID], stage)
|
|
}
|
|
|
|
// HandleUserInteractiveFlow will direct and complete auth flow stages
|
|
// that the client has requested. This function requires http request, session Id
|
|
// authentication data as AuthDict object, config object and a config.UserInteractiveAuthConfig
|
|
// object containing list of allowed auth flows and params for successful authentication.
|
|
// It returns nil if auth is successful else returns jsonResponse (can be sent directly to
|
|
// client as http response) containing userInteractiveResponse or relevant error (if encountered).
|
|
func HandleUserInteractiveFlow(
|
|
req *http.Request,
|
|
auth AuthDict,
|
|
sessionID string,
|
|
cfg *config.Dendrite,
|
|
authCfg config.UserInteractiveAuthConfig,
|
|
) *util.JSONResponse {
|
|
|
|
switch auth.Type {
|
|
case authtypes.LoginTypeRecaptcha:
|
|
// Check given captcha response
|
|
resErr := validateRecaptcha(cfg, auth.Response, req.RemoteAddr)
|
|
if resErr != nil {
|
|
return resErr
|
|
}
|
|
|
|
// Add Recaptcha to the list of completed authentication stages
|
|
AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
|
|
|
// A missing auth type means the request is made as the first step of a authentication
|
|
// using the User-Interactive Authentication API. Skip the default case for this.
|
|
case "":
|
|
|
|
case authtypes.LoginTypeDummy:
|
|
// there is nothing to do
|
|
// Add Dummy to the list of completed authentication stages
|
|
AddCompletedSessionStage(sessionID, authtypes.LoginTypeDummy)
|
|
|
|
default:
|
|
return &util.JSONResponse{
|
|
Code: http.StatusNotImplemented,
|
|
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
|
}
|
|
}
|
|
|
|
// Check if the user's authentication flow has been completed successfully
|
|
// A response with current authentication flow and remaining available methods
|
|
// will be returned if a flow has not been successfully completed yet
|
|
return checkAndCompleteFlow(sessions.GetCompletedStages(sessionID), sessionID, authCfg)
|
|
}
|
|
|
|
// checkAndCompleteFlow checks if authentication flow is completed given
|
|
// a set of allowed stages. If so, authentication is completed and
|
|
// function returns nil, otherwise a userInteractiveResponse
|
|
// with required stages is returned.
|
|
func checkAndCompleteFlow(
|
|
flow []authtypes.LoginType,
|
|
sessionID string,
|
|
authCfg config.UserInteractiveAuthConfig,
|
|
) *util.JSONResponse {
|
|
if checkFlowCompleted(flow, authCfg.Flows) {
|
|
// This flow was completed, authentication successful.
|
|
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,
|
|
authCfg.Flows, authCfg.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 authentication 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
|
|
}
|
|
|
|
// 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 []int `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.StatusConflict,
|
|
JSON: jsonerror.Unknown("Captcha authentication 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.StatusGatewayTimeout,
|
|
JSON: jsonerror.Unknown("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
|
|
}
|
|
|
|
type availableResponse struct {
|
|
Available bool `json:"available"`
|
|
}
|