diff options
author | Corey Hulen <corey@hulen.com> | 2016-04-21 22:37:01 -0700 |
---|---|---|
committer | Corey Hulen <corey@hulen.com> | 2016-04-21 22:37:01 -0700 |
commit | 2e5617c29be69637acd384e85f795a0b343bec8d (patch) | |
tree | 6b8bdae1e664013b97c2dda94985375abda91aa5 /api/oauth.go | |
parent | 5c755463ed3a4c74a383fb4460b5be02d8868481 (diff) | |
download | chat-2e5617c29be69637acd384e85f795a0b343bec8d.tar.gz chat-2e5617c29be69637acd384e85f795a0b343bec8d.tar.bz2 chat-2e5617c29be69637acd384e85f795a0b343bec8d.zip |
PLT-2057 User as a first class object (#2648)
* Adding TeamMember to system
* Fixing all unit tests on the backend
* Fixing merge conflicts
* Fixing merge conflict
* Adding javascript unit tests
* Adding TeamMember to system
* Fixing all unit tests on the backend
* Fixing merge conflicts
* Fixing merge conflict
* Adding javascript unit tests
* Adding client side unit test
* Cleaning up the clint side tests
* Fixing msg
* Adding more client side unit tests
* Adding more using tests
* Adding last bit of client side unit tests and adding make cmd
* Fixing bad merge
* Fixing libraries
* Updating to new client side API
* Fixing borken unit test
* Fixing unit tests
* ugg...trying to beat gofmt
* ugg...trying to beat gofmt
* Cleaning up remainder of the server side routes
* Adding inital load api
* Increased coverage of webhook unit tests (#2660)
* Adding loading ... to root html
* Fixing bad merge
* Removing explicit content type so superagent will guess corectly (#2685)
* Fixing merge and unit tests
* Adding create team UI
* Fixing signup flows
* Adding LDAP unit tests and enterprise unit test helper (#2702)
* Add the ability to reset MFA from the commandline (#2706)
* Fixing compliance unit tests
* Fixing client side tests
* Adding open server to system console
* Moving websocket connection
* Fixing unit test
* Fixing unit tests
* Fixing unit tests
* Adding nickname and more LDAP unit tests (#2717)
* Adding join open teams
* Cleaning up all TODOs in the code
* Fixing web sockets
* Removing unused webockets file
* PLT-2533 Add the ability to reset a user's MFA from the system console (#2715)
* Add the ability to reset a user's MFA from the system console
* Add client side unit test for adminResetMfa
* Reorganizing authentication to fix LDAP error message (#2723)
* Fixing failing unit test
* Initial upgrade db code
* Adding upgrade script
* Fixing upgrade script after running on core
* Update OAuth and Claim routes to work with user model changes (#2739)
* Fixing perminant deletion. Adding ability to delete all user and the entire database (#2740)
* Fixing team invite ldap login call (#2741)
* Fixing bluebar and some img stuff
* Fix all the different file upload web utils (#2743)
* Fixing invalid session redirect (#2744)
* Redirect on bad channel name (#2746)
* Fixing a bunch of issue and removing dead code
* Patch to fix error message on leave channel (#2747)
* Setting EnableOpenServer to false by default
* Fixing config
* Fixing upgrade
* Fixing reported bugs
* Bug fixes for PLT-2057
* PLT-2563 Redo password recovery to use a database table (#2745)
* Redo password recovery to use a database table
* Update reset password audits
* Split out admin and user reset password APIs to be separate
* Delete password recovery when user is permanently deleted
* Consolidate password resetting into a single function
* Removed private channels as an option for outgoing webhooks (#2752)
* PLT-2577/PLT-2552 Fixes for backstage (#2753)
* Added URL to incoming webhook list
* Fixed client functions for adding/removing integrations
* Disallowed slash commands without trigger words
* Fixed clientside handling of errors on AddCommand page
* Minor auth cleanup (#2758)
* Changed EditPostModal to just close if you save without making any changes (#2759)
* Renamed client -> Client in async_client.jsx and fixed eslint warnings (#2756)
* Fixed url in channel info modal (#2755)
* Fixing reported issues
* Moving to version 3 of the apis
* Fixing command unit tests (#2760)
* Adding team admins
* Fixing DM issue
* Fixing eslint error
* Properly set EditPostModal's originalText state in all cases (#2762)
* Update client config check to assume features is defined if server is licensed (#2772)
* Fixing url link
* Fixing issue with websocket crashing when sending messages to different teams
Diffstat (limited to 'api/oauth.go')
-rw-r--r-- | api/oauth.go | 256 |
1 files changed, 193 insertions, 63 deletions
diff --git a/api/oauth.go b/api/oauth.go index a7119d7e5..0375f4e6f 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -4,7 +4,10 @@ package api import ( + "crypto/tls" + b64 "encoding/base64" "fmt" + "io" "net/http" "net/url" "strconv" @@ -12,31 +15,29 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) -func InitOAuth(r *mux.Router) { +func InitOAuth() { l4g.Debug(utils.T("api.oauth.init.debug")) - sr := r.PathPrefix("/oauth").Subrouter() + BaseRoutes.OAuth.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") + BaseRoutes.OAuth.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + BaseRoutes.OAuth.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") - sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") - sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") - sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") - sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") - sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") - sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") - sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") - - mr := Srv.Router - mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") - mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + BaseRoutes.Root.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") // Handle all the old routes, to be later removed - mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") - mr.Handle("/signup/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") - mr.Handle("/login/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/signup/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + BaseRoutes.Root.Handle("/login/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") } func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -190,40 +191,40 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { uri := c.GetSiteURL() + "/signup/" + service + "/complete" - if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { + if body, teamId, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { c.Err = err return } else { action := props["action"] switch action { case model.OAUTH_ACTION_SIGNUP: - CreateOAuthUser(c, w, r, service, body, team) + CreateOAuthUser(c, w, r, service, body, teamId) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } break case model.OAUTH_ACTION_LOGIN: - LoginByOAuth(c, w, r, service, body, team) + LoginByOAuth(c, w, r, service, body) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } break case model.OAUTH_ACTION_EMAIL_TO_SSO: - CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + CompleteSwitchWithOAuth(c, w, r, service, body, props["email"]) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect) } break case model.OAUTH_ACTION_SSO_TO_EMAIL: - LoginByOAuth(c, w, r, service, body, team) + LoginByOAuth(c, w, r, service, body) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) } break default: - LoginByOAuth(c, w, r, service, body, team) + LoginByOAuth(c, w, r, service, body) if c.Err == nil { - http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) } break } @@ -257,7 +258,7 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } var team *model.Team - if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil { c.Err = result.Err return } else { @@ -389,7 +390,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true} if result := <-Srv.Store.Session().Save(session); result.Err != nil { c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") @@ -422,24 +423,11 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] loginHint := r.URL.Query().Get("login_hint") - teamName := r.URL.Query().Get("team") - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - // Make sure team exists - if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } stateProps := map[string]string{} stateProps["action"] = model.OAUTH_ACTION_LOGIN - if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { + if authUrl, err := GetAuthorizationCode(c, service, stateProps, loginHint); err != nil { c.Err = err return } else { @@ -450,31 +438,19 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] - teamName := r.URL.Query().Get("team") if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } - if len(teamName) == 0 { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - hash := r.URL.Query().Get("h") - var team *model.Team - if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } + teamId := "" + inviteId := r.URL.Query().Get("id") - if IsVerifyHashRequired(nil, team, hash) { + if len(hash) > 0 { data := r.URL.Query().Get("d") props := model.MapFromJson(strings.NewReader(data)) @@ -489,19 +465,173 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } - if team.Id != props["id"] { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) - return + teamId = props["id"] + } else if len(inviteId) != 0 { + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + // soft fail, so we still create user but don't auto-join team + l4g.Error("%v", result.Err) + } else { + teamId = result.Data.(*model.Team).Id } } stateProps := map[string]string{} stateProps["action"] = model.OAUTH_ACTION_SIGNUP + if len(teamId) != 0 { + stateProps["team_id"] = teamId + } - if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil { c.Err = err return } else { http.Redirect(w, r, authUrl, http.StatusFound) } } + +func GetAuthorizationCode(c *Context, service string, props map[string]string, loginHint string) (string, *model.AppError) { + + sso := utils.Cfg.GetSSOService(service) + if sso != nil && !sso.Enable { + return "", model.NewLocAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service) + } + + clientId := sso.Id + endpoint := sso.AuthEndpoint + scope := sso.Scope + + props["hash"] = model.HashPassword(clientId) + state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) + + redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" + + authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) + + if len(scope) > 0 { + authUrl += "&scope=" + utils.UrlEncode(scope) + } + + if len(loginHint) > 0 { + authUrl += "&login_hint=" + utils.UrlEncode(loginHint) + } + + return authUrl, nil +} + +func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) { + sso := utils.Cfg.GetSSOService(service) + if sso == nil || !sso.Enable { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.unsupported.app_error", nil, "service="+service) + } + + stateStr := "" + if b, err := b64.StdEncoding.DecodeString(state); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error()) + } else { + stateStr = string(b) + } + + stateProps := model.MapFromJson(strings.NewReader(stateStr)) + + if !model.ComparePassword(stateProps["hash"], sso.Id) { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.invalid_state.app_error", nil, "") + } + + teamId := stateProps["team_id"] + + p := url.Values{} + p.Set("client_id", sso.Id) + p.Set("client_secret", sso.Secret) + p.Set("code", code) + p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + p.Set("redirect_uri", redirectUri) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} + req, _ := http.NewRequest("POST", sso.TokenEndpoint, strings.NewReader(p.Encode())) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + var ar *model.AccessResponse + if resp, err := client.Do(req); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error()) + } else { + ar = model.AccessResponseFromJson(resp.Body) + if ar == nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_response.app_error", nil, "") + } + } + + if strings.ToLower(ar.TokenType) != model.ACCESS_TOKEN_TYPE { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.bad_token.app_error", nil, "token_type="+ar.TokenType) + } + + if len(ar.AccessToken) == 0 { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.missing.app_error", nil, "") + } + + p = url.Values{} + p.Set("access_token", ar.AccessToken) + req, _ = http.NewRequest("GET", sso.UserApiEndpoint, strings.NewReader("")) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+ar.AccessToken) + + if resp, err := client.Do(req); err != nil { + return nil, "", nil, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", + map[string]interface{}{"Service": service}, err.Error()) + } else { + return resp.Body, teamId, stateProps, nil + } + +} + +func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, email string) { + authData := "" + ssoEmail := "" + provider := einterfaces.GetOauthProvider(service) + if provider == nil { + c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error", + map[string]interface{}{"Service": service}, "") + return + } else { + ssoUser := provider.GetUserFromJson(userData) + authData = ssoUser.AuthData + ssoEmail = ssoUser.Email + } + + if len(authData) == 0 { + c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error", + map[string]interface{}{"Service": service}, "") + return + } + + if len(email) == 0 { + c.Err = model.NewLocAppError("CompleteClaimWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "") + return + } + + var user *model.User + if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + } + + RevokeAllSession(c, user.Id) + if c.Err != nil { + return + } + + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, authData, ssoEmail); result.Err != nil { + c.Err = result.Err + return + } + + sendSignInChangeEmailAndForget(c, user.Email, c.GetSiteURL(), strings.Title(service)+" SSO") +} |