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

Commit

Permalink
Implement SSO login initial changes
Browse files Browse the repository at this point in the history
Co-authored-by: Rohit Mohan <[email protected]>
  • Loading branch information
Anand Vasudevan and rohitmohan96 committed Sep 1, 2020
1 parent 3f9b829 commit 9b57935
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 1 deletion.
16 changes: 15 additions & 1 deletion clientapi/routing/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,30 @@ func passwordLogin() flows {
return f
}

func ssoLogin() flows {
f := flows{}
s := flow{
Type: "m.login.sso",
Stages: []string{"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{
Expand Down
6 changes: 6 additions & 0 deletions clientapi/routing/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ func Setup(
}),
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)

r0mux.Handle("/login/sso/redirect",
httputil.MakeExternalAPI("login", func(req *http.Request) util.JSONResponse {
return SSORedirect(req, accountDB, cfg)
}),
).Methods(http.MethodGet, http.MethodOptions)

r0mux.Handle("/auth/{authType}/fallback/web",
httputil.MakeHTMLAPI("auth_fallback", func(w http.ResponseWriter, req *http.Request) *util.JSONResponse {
vars := mux.Vars(req)
Expand Down
198 changes: 198 additions & 0 deletions clientapi/routing/sso.go
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,
}

}
5 changes: 5 additions & 0 deletions dendrite-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ client_api:
recaptcha_bypass_secret: ""
recaptcha_siteverify_api: ""

cas:
cas_enabled: true
cas_server: "http://example.com"
cas_validate_endpoint: "/proxyValidate"

# TURN server information that this homeserver should send to clients.
turn:
turn_user_lifetime: ""
Expand Down
18 changes: 18 additions & 0 deletions internal/config/config_clientapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type ClientAPI struct {
// was successful
RecaptchaSiteVerifyAPI string `yaml:"recaptcha_siteverify_api"`

// CAS server settings
CAS CAS `yaml:"cas"`

// TURN options
TURN TURN `yaml:"turn"`
}
Expand All @@ -47,6 +50,7 @@ func (c *ClientAPI) Defaults() {
c.RecaptchaBypassSecret = ""
c.RecaptchaSiteVerifyAPI = ""
c.RegistrationDisabled = false
c.CAS.Enabled = false
}

func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) {
Expand All @@ -61,6 +65,20 @@ func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) {
checkNotEmpty(configErrs, "client_api.recaptcha_siteverify_api", string(c.RecaptchaSiteVerifyAPI))
}
c.TURN.Verify(configErrs)
c.CAS.Verify(configErrs)
}

type CAS struct {
Enabled bool `yaml:"cas_enabled"`
Server string `yaml:"cas_server"`
ValidateEndpoint string `yaml:"cas_validate_endpoint"`
}

func (cas *CAS) Verify(ConfigErrors *ConfigErrors) {
if cas.Enabled {
checkURL(ConfigErrors, "client_api.cas.cas_server", cas.Server)
checkNotEmpty(ConfigErrors, "client_api.cas.cas_validate_endpoint", cas.ValidateEndpoint)
}
}

type TURN struct {
Expand Down

0 comments on commit 9b57935

Please sign in to comment.