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
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Rohit Mohan <[email protected]>
- Loading branch information
1 parent
3f9b829
commit 9b57935
Showing
5 changed files
with
242 additions
and
1 deletion.
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
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
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,198 @@ | ||
// Copyright 2017 Vector Creations 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/xml" | ||
"io/ioutil" | ||
"net/http" | ||
"net/url" | ||
|
||
"github.com/matrix-org/dendrite/clientapi/jsonerror" | ||
"github.com/matrix-org/dendrite/internal/config" | ||
"github.com/matrix-org/dendrite/internal/sqlutil" | ||
"github.com/matrix-org/dendrite/userapi/storage/accounts" | ||
"github.com/matrix-org/util" | ||
) | ||
|
||
// the XML response structure of CAS ticket validation | ||
type casValidateResponse struct { | ||
XMLName xml.Name `xml:"serviceResponse"` | ||
Cas string `xml:"cas,attr"` | ||
AuthenticationSuccess struct { | ||
User string `xml:"user"` | ||
} `xml:"authenticationSuccess"` | ||
} | ||
|
||
// SSORedirect implements GET /login/sso/redirect | ||
func SSORedirect( | ||
req *http.Request, | ||
accountDB accounts.Database, | ||
cfg *config.ClientAPI, | ||
) util.JSONResponse { | ||
// if dendrite is not configured to use SSO by the admin return bad method | ||
if !cfg.CAS.Enabled || cfg.CAS.Server == "" { | ||
return util.JSONResponse{ | ||
Code: http.StatusMethodNotAllowed, | ||
JSON: jsonerror.NotFound("Bad method"), | ||
} | ||
} | ||
|
||
// try to parse the SSO URL configured to a url.URL type | ||
ssoURL, err := url.Parse(cfg.CAS.Server) | ||
if err != nil { | ||
return util.JSONResponse{ | ||
Code: http.StatusInternalServerError, | ||
JSON: jsonerror.Unknown("Failed to parse SSO URL configured: " + err.Error()), | ||
} | ||
} | ||
|
||
// a redirect URL is required for this endpoint | ||
redirectURL := req.FormValue("redirectUrl") | ||
if redirectURL == "" { | ||
return util.JSONResponse{ | ||
Code: http.StatusBadRequest, | ||
JSON: jsonerror.MissingArgument("redirectUrl parameter missing"), | ||
} | ||
} | ||
|
||
// if the request has a ticket param, validate the ticket instead of redirecting | ||
// this is because the after validation, the CAS server redirects the browser to the | ||
// "service" url that we send with an added param "ticket" | ||
if ticket := req.FormValue("ticket"); ticket != "" { | ||
return SSOTicket(req, accountDB, cfg) | ||
} | ||
|
||
// add the params to the sso url | ||
ssoQueries := make(url.Values) | ||
// the service url that we send to CAS is homeserver.com/_matrix/client/r0/login/sso/redirect?redirectUrl=xyz | ||
ssoQueries.Set("service", req.RequestURI) | ||
|
||
ssoURL.RawQuery = ssoQueries.Encode() | ||
|
||
return util.RedirectResponse(ssoURL.String()) | ||
} | ||
|
||
// gets the ticket from the SSO server (this is different from the homeserver login/access token) | ||
// generates an login token for the client to login to the homeserver with | ||
func SSOTicket( | ||
req *http.Request, | ||
accountDB accounts.Database, | ||
cfg *config.ClientAPI, | ||
) util.JSONResponse { | ||
// form the ticket validation URL from the config | ||
ssoURL, err := url.Parse(cfg.CAS.Server + cfg.CAS.ValidateEndpoint) | ||
if err != nil { | ||
return util.JSONResponse{ | ||
Code: http.StatusInternalServerError, | ||
JSON: jsonerror.Unknown("Failed to parse SSO URL configured: " + err.Error()), | ||
} | ||
} | ||
|
||
ticket := req.FormValue("ticket") | ||
|
||
// append required params to the CAS validate endpoint | ||
ssoQueries := make(url.Values) | ||
ssoQueries.Set("ticket", ticket) | ||
ssoURL.RawQuery = ssoQueries.Encode() | ||
|
||
// validate the ticket | ||
casUsername, err := validateTicket(ticket, cfg) | ||
if err != nil { | ||
// TODO: should I be logging these? What else should I log? | ||
util.GetLogger(req.Context()).WithError(err).Error("CAS SSO ticket validation failed") | ||
return util.JSONResponse{ | ||
Code: http.StatusUnauthorized, | ||
JSON: jsonerror.Unknown("Could not validate SSO token: " + err.Error()), | ||
} | ||
} | ||
if casUsername == "" { | ||
util.GetLogger(req.Context()).WithError(err).Error("CAS SSO returned no user") | ||
return util.JSONResponse{ | ||
Code: http.StatusUnauthorized, | ||
JSON: jsonerror.Unknown("CAS SSO returned no user"), | ||
} | ||
} | ||
|
||
// ticket validated. Login the user | ||
return completeSSOAuth(req, casUsername, accountDB) | ||
} | ||
|
||
// pass the ticket to the sso server to get it validated | ||
// the CAS server responds with an xml which contains the username | ||
func validateTicket( | ||
ssoURL string, | ||
cfg *config.ClientAPI, | ||
) (string, error) { | ||
// make the call to the sso server to validate | ||
response, err := http.Get(ssoURL) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
// extract the response from the sso server | ||
data, err := ioutil.ReadAll(response.Body) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
// parse the response to the CAS XML format | ||
var res casValidateResponse | ||
if err := xml.Unmarshal([]byte(data), &res); err != nil { | ||
return "", err | ||
} | ||
|
||
return res.AuthenticationSuccess.User, nil | ||
} | ||
|
||
// returns with a redirect to the redirectUrl | ||
func completeSSOAuth( | ||
req *http.Request, | ||
username string, | ||
accountDB accounts.Database, | ||
) util.JSONResponse { | ||
// try to create an account with that username | ||
// if the user exists, then we pick that user, else we create a new user | ||
account, err := accountDB.CreateAccount(req.Context(), username, "", "") | ||
if err != nil { | ||
// some error | ||
if err != sqlutil.ErrUserExists { | ||
return util.JSONResponse{ | ||
Code: http.StatusUnauthorized, | ||
JSON: jsonerror.Unknown("Could not create new user"), | ||
} | ||
} else { | ||
// user already exists, so just pick up their details | ||
account, err = accountDB.GetAccountByLocalpart(req.Context(), username) | ||
if err != nil { | ||
return util.JSONResponse{ | ||
Code: http.StatusUnauthorized, | ||
JSON: jsonerror.Unknown("Could not query user"), | ||
} | ||
} | ||
} | ||
} | ||
|
||
// give the user a login token to use for login | ||
// The generated token should be a macaroon, suitable for use with the m.login.token type of the /login API, and token-based interactive login. The lifetime of this token SHOULD be limited to around five seconds. This token is given to the client via the loginToken query parameter previously mentioned. | ||
// Note that a login token is separate from an access token, the latter providing general authentication to various API endpoints. | ||
|
||
return util.JSONResponse{ | ||
// TODO: change this to a redirect after implementing the token part | ||
Code: http.StatusOK, | ||
JSON: account, | ||
} | ||
|
||
} |
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
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