This repository has been archived by the owner on Nov 25, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 678
[WIP] Implement m.login.token and SSO login initial changes #1374
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
// Copyright 2020 The Matrix.org Foundation C.I.C. | ||
// | ||
// 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 auth | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"errors" | ||
"net/http" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/matrix-org/dendrite/clientapi/jsonerror" | ||
"github.com/matrix-org/dendrite/clientapi/userutil" | ||
"github.com/matrix-org/dendrite/internal/config" | ||
"github.com/matrix-org/dendrite/userapi/api" | ||
"github.com/matrix-org/gomatrixserverlib" | ||
"github.com/matrix-org/util" | ||
) | ||
|
||
// This file handles all the m.login.token logic | ||
|
||
// GetAccountByLocalpart function implemented by the appropriate database type | ||
type GetAccountByLocalpart func(ctx context.Context, localpart string) (*api.Account, error) | ||
|
||
// LoginTokenRequest struct to hold the possible parameters from an m.login.token http request | ||
type LoginTokenRequest struct { | ||
Login | ||
Token string `json:"token"` | ||
TxnID string `json:"txn_id"` | ||
} | ||
|
||
// LoginTypeToken holds the configs and the appropriate GetAccountByLocalpart function for the database | ||
type LoginTypeToken struct { | ||
GetAccountByLocalpart GetAccountByLocalpart | ||
Config *config.ClientAPI | ||
} | ||
|
||
// Name returns the expected type of "m.login.token" | ||
func (t *LoginTypeToken) Name() string { | ||
return "m.login.token" | ||
} | ||
|
||
// Request returns a struct of type LoginTokenRequest | ||
func (t *LoginTypeToken) Request() interface{} { | ||
return &LoginTokenRequest{} | ||
} | ||
|
||
// Type of the LoginToken | ||
type loginToken struct { | ||
UserID string | ||
CreationTime int64 | ||
RandomPart string | ||
} | ||
|
||
// Login completes the whole token validation, user verification for m.login.token | ||
// returns a struct of type *auth.Login which has the users details | ||
func (t *LoginTypeToken) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) { | ||
r := req.(*LoginTokenRequest) | ||
userID, err := validateLoginToken(r.Token, r.TxnID, &t.Config.Matrix.ServerName) | ||
if err != nil { | ||
return nil, &util.JSONResponse{ | ||
Code: http.StatusUnauthorized, | ||
JSON: jsonerror.InvalidArgumentValue(err.Error()), | ||
} | ||
} | ||
r.Login.Identifier.User = userID | ||
r.Login.Identifier.Type = "m.id.user" | ||
|
||
return &r.Login, nil | ||
} | ||
|
||
// Decodes and validates a LoginToken | ||
// Accepts the base64 encoded token string as param | ||
// Checks the time expiry, userID (only the format, doesn't check to see if the user exists) | ||
// Also checks the DB to see if the token exists | ||
// Returns the localpart if successful | ||
func validateLoginToken(tokenStr string, txnID string, serverName *gomatrixserverlib.ServerName) (string, error) { | ||
token, err := decodeLoginToken(tokenStr) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
// check whether the token has a valid time. | ||
// TODO: should this 5 second window be configurable? | ||
if time.Now().Unix()-token.CreationTime > 5 { | ||
return "", errors.New("Token has expired") | ||
} | ||
|
||
// check whether the UserID is malformed | ||
if !strings.Contains(token.UserID, "@") { | ||
// TODO: should we reveal details about the error with the token or give vague responses instead? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revealing details is fine for now. |
||
return "", errors.New("Invalid UserID") | ||
} | ||
if _, err := userutil.ParseUsernameParam(token.UserID, serverName); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add some prose here to explain why this check alone is insufficient (it doesn't check there is an |
||
return "", err | ||
} | ||
|
||
// check in the database | ||
if err := checkDBToken(tokenStr, txnID); err != nil { | ||
return "", err | ||
} | ||
|
||
return token.UserID, nil | ||
} | ||
|
||
// GenerateLoginToken creates a login token which is a base64 encoded string of (userID+time+random) | ||
// returns an error if it cannot create a random string | ||
func GenerateLoginToken(userID string) (string, error) { | ||
// the time of token creation | ||
timePart := []byte(strconv.FormatInt(time.Now().Unix(), 10)) | ||
|
||
// the random part of the token | ||
randPart := make([]byte, 10) | ||
if _, err := rand.Read(randPart); err != nil { | ||
return "", err | ||
} | ||
|
||
// url-safe no padding | ||
return base64.RawURLEncoding.EncodeToString([]byte(userID)) + "." + base64.RawURLEncoding.EncodeToString(timePart) + "." + base64.RawURLEncoding.EncodeToString(randPart), nil | ||
} | ||
|
||
// Decodes the given tokenStr into a LoginToken struct | ||
func decodeLoginToken(tokenStr string) (*loginToken, error) { | ||
// split the string into it's constituent parts | ||
strParts := strings.Split(tokenStr, ".") | ||
if len(strParts) != 3 { | ||
return nil, errors.New("Malformed token string") | ||
} | ||
|
||
var token loginToken | ||
// decode each of the strParts | ||
userBytes, err := base64.RawURLEncoding.DecodeString(strParts[0]) | ||
if err != nil { | ||
return nil, errors.New("Invalid user ID") | ||
} | ||
token.UserID = string(userBytes) | ||
|
||
// first decode the time to a string | ||
timeBytes, err := base64.RawURLEncoding.DecodeString(strParts[1]) | ||
if err != nil { | ||
return nil, errors.New("Invalid creation time") | ||
} | ||
// now convert the string to an integer | ||
creationTime, err := strconv.ParseInt(string(timeBytes), 10, 64) | ||
if err != nil { | ||
return nil, errors.New("Invalid creation time") | ||
} | ||
token.CreationTime = creationTime | ||
|
||
randomBytes, err := base64.RawURLEncoding.DecodeString(strParts[2]) | ||
if err != nil { | ||
return nil, errors.New("Invalid random part") | ||
} | ||
token.UserID = string(randomBytes) | ||
|
||
token = loginToken{ | ||
UserID: string(userBytes), | ||
CreationTime: creationTime, | ||
RandomPart: string(randomBytes), | ||
} | ||
return &token, nil | ||
} | ||
|
||
// Checks whether the token exists in the DB and whether the token is assigned to the current transaction ID | ||
// Does not validate the userID or the creation time expiry | ||
// Returns nil if successful | ||
func checkDBToken(tokenStr string, txnID string) error { | ||
// if the client has provided a transaction id, try to lock the token to that ID | ||
if txnID != "" { | ||
if err := LinkToken(tokenStr, txnID); err != nil { | ||
// TODO: should we abort the login attempt or something else? | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// StoreLoginToken stores the login token in the database | ||
// Returns nil if successful | ||
func StoreLoginToken(tokenStr string) error { | ||
return nil | ||
} | ||
|
||
// DeleteLoginToken Deletes a token from the DB | ||
// used to delete a token that has already been used | ||
// Returns nil if successful | ||
func DeleteLoginToken(tokenStr string) error { | ||
return nil | ||
} | ||
|
||
// LinkToken Links a token to a transaction ID so no other client can try to login using that token | ||
// as specified in https://matrix.org/docs/spec/client_server/r0.6.1#token-based | ||
func LinkToken(tokenStr string, txnID string) error { | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,10 @@ | |
package routing | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"io/ioutil" | ||
"net/http" | ||
|
||
"github.com/matrix-org/dendrite/clientapi/auth" | ||
|
@@ -53,40 +56,127 @@ func passwordLogin() flows { | |
return f | ||
} | ||
|
||
func ssoLogin() flows { | ||
f := flows{} | ||
s := flow{ | ||
Type: "m.login.sso", | ||
} | ||
f.Flows = append(f.Flows, s) | ||
return f | ||
} | ||
|
||
// Login implements GET and POST /login | ||
func Login( | ||
req *http.Request, accountDB accounts.Database, userAPI userapi.UserInternalAPI, | ||
cfg *config.ClientAPI, | ||
) util.JSONResponse { | ||
if req.Method == http.MethodGet { | ||
// TODO: support other forms of login other than password, depending on config options | ||
flows := passwordLogin() | ||
if cfg.CAS.Enabled { | ||
flows.Flows = append(flows.Flows, ssoLogin().Flows...) | ||
} | ||
return util.JSONResponse{ | ||
Code: http.StatusOK, | ||
JSON: passwordLogin(), | ||
JSON: flows, | ||
} | ||
} else if req.Method == http.MethodPost { | ||
typePassword := auth.LoginTypePassword{ | ||
GetAccountByPassword: accountDB.GetAccountByPassword, | ||
Config: cfg, | ||
// TODO: is the the right way to read the body and re-add it? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
body, err := ioutil.ReadAll(req.Body) | ||
if err != nil { | ||
return util.JSONResponse{ | ||
Code: http.StatusBadRequest, | ||
JSON: jsonerror.BadJSON("Bad JSON"), | ||
} | ||
} | ||
r := typePassword.Request() | ||
resErr := httputil.UnmarshalJSONRequest(req, r) | ||
if resErr != nil { | ||
return *resErr | ||
// add the body back to the request because ioutil.ReadAll consumes the body | ||
req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) | ||
|
||
// marshall the body into an unstructured json map | ||
var jsonBody map[string]interface{} | ||
if err := json.Unmarshal([]byte(body), &jsonBody); err != nil { | ||
kegsay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return util.JSONResponse{ | ||
Code: http.StatusBadRequest, | ||
JSON: jsonerror.BadJSON("Bad JSON"), | ||
} | ||
} | ||
|
||
var loginType string | ||
if val, ok := jsonBody["type"]; ok { | ||
loginType = val.(string) | ||
} else { | ||
return util.JSONResponse{ | ||
Code: http.StatusBadRequest, | ||
JSON: jsonerror.BadJSON("No 'type' parameter"), | ||
} | ||
} | ||
login, authErr := typePassword.Login(req.Context(), r) | ||
if authErr != nil { | ||
return *authErr | ||
if loginType == "m.login.password" { | ||
return doPasswordLogin(req, accountDB, userAPI, cfg) | ||
} else if loginType == "m.login.token" { | ||
return doTokenLogin(req, accountDB, userAPI, cfg) | ||
} | ||
// make a device/access token | ||
return completeAuth(req.Context(), cfg.Matrix.ServerName, userAPI, login) | ||
} | ||
|
||
return util.JSONResponse{ | ||
Code: http.StatusMethodNotAllowed, | ||
JSON: jsonerror.NotFound("Bad method"), | ||
} | ||
} | ||
|
||
// Handles a m.login.password login type request | ||
func doPasswordLogin( | ||
req *http.Request, accountDB accounts.Database, userAPI userapi.UserInternalAPI, | ||
cfg *config.ClientAPI, | ||
) util.JSONResponse { | ||
typePassword := auth.LoginTypePassword{ | ||
GetAccountByPassword: accountDB.GetAccountByPassword, | ||
Config: cfg, | ||
} | ||
r := typePassword.Request() | ||
resErr := httputil.UnmarshalJSONRequest(req, r) | ||
if resErr != nil { | ||
return *resErr | ||
} | ||
login, authErr := typePassword.Login(req.Context(), r) | ||
if authErr != nil { | ||
return *authErr | ||
} | ||
|
||
// make a device/access token | ||
return completeAuth(req.Context(), cfg.Matrix.ServerName, userAPI, login) | ||
} | ||
|
||
// Handles a m.login.token login type request | ||
func doTokenLogin(req *http.Request, accountDB accounts.Database, userAPI userapi.UserInternalAPI, | ||
cfg *config.ClientAPI, | ||
) util.JSONResponse { | ||
// create a struct with the appropriate DB(postgres/sqlite) function and the configs | ||
typeToken := auth.LoginTypeToken{ | ||
GetAccountByLocalpart: accountDB.GetAccountByLocalpart, | ||
Config: cfg, | ||
} | ||
r := typeToken.Request() | ||
resErr := httputil.UnmarshalJSONRequest(req, r) | ||
if resErr != nil { | ||
return *resErr | ||
} | ||
login, authErr := typeToken.Login(req.Context(), r) | ||
if authErr != nil { | ||
return *authErr | ||
} | ||
|
||
// make a device/access token | ||
authResult := completeAuth(req.Context(), cfg.Matrix.ServerName, userAPI, login) | ||
|
||
// the login is successful, delete the login token before returning the access token to the client | ||
if authResult.Code == http.StatusOK { | ||
if err := auth.DeleteLoginToken(r.(*auth.LoginTokenRequest).Token); err != nil { | ||
util.GetLogger(req.Context()).WithError(err).Error("Could not delete login ticket from DB") | ||
} | ||
} | ||
return authResult | ||
} | ||
|
||
func completeAuth( | ||
ctx context.Context, serverName gomatrixserverlib.ServerName, userAPI userapi.UserInternalAPI, login *auth.Login, | ||
) util.JSONResponse { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5 seconds seems very short, perhaps 1 minute?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only choose 5 seconds because the spec suggested it: https://matrix.org/docs/spec/client_server/r0.6.1#handling-the-authentication-endpoint
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow I see. In which case please just link to this on this line to justify why 5s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5 seconds sounds pretty short to me as well, if you have a connection or DNS failure that could easily time out. It seems that Synapse uses 2 minutes, although I was unable to find any reference to why that was chosen: https://github.com/matrix-org/synapse/blob/78d5f91de1a9baf4dbb0a794cb49a799f29f7a38/synapse/handlers/auth.py#L1323-L1325
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, should I make this time configurable by the admin (with a default time of 5 seconds)?