github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/web/oauth.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package web
     5  
     6  import (
     7  	"net/http"
     8  	"net/url"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/vnforks/kid/v5/app"
    13  	"github.com/vnforks/kid/v5/audit"
    14  	"github.com/vnforks/kid/v5/mlog"
    15  	"github.com/vnforks/kid/v5/model"
    16  	"github.com/vnforks/kid/v5/utils"
    17  	"github.com/vnforks/kid/v5/utils/fileutils"
    18  )
    19  
    20  func (w *Web) InitOAuth() {
    21  	// API version independent OAuth 2.0 as a service provider endpoints
    22  	w.MainRouter.Handle("/oauth/authorize", w.ApiHandlerTrustRequester(authorizeOAuthPage)).Methods("GET")
    23  	w.MainRouter.Handle("/oauth/authorize", w.ApiSessionRequired(authorizeOAuthApp)).Methods("POST")
    24  	w.MainRouter.Handle("/oauth/deauthorize", w.ApiSessionRequired(deauthorizeOAuthApp)).Methods("POST")
    25  	w.MainRouter.Handle("/oauth/access_token", w.ApiHandlerTrustRequester(getAccessToken)).Methods("POST")
    26  
    27  	// API version independent OAuth as a client endpoints
    28  	w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET")
    29  	w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/login", w.ApiHandler(loginWithOAuth)).Methods("GET")
    30  	w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/mobile_login", w.ApiHandler(mobileLoginWithOAuth)).Methods("GET")
    31  	w.MainRouter.Handle("/oauth/{service:[A-Za-z0-9]+}/signup", w.ApiHandler(signupWithOAuth)).Methods("GET")
    32  
    33  	// Old endpoints for backwards compatibility, needed to not break SSO for any old setups
    34  	w.MainRouter.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET")
    35  	w.MainRouter.Handle("/signup/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET")
    36  	w.MainRouter.Handle("/login/{service:[A-Za-z0-9]+}/complete", w.ApiHandler(completeOAuth)).Methods("GET")
    37  	w.MainRouter.Handle("/api/v4/oauth_test", w.ApiSessionRequired(testHandler)).Methods("GET")
    38  }
    39  
    40  func testHandler(c *Context, w http.ResponseWriter, r *http.Request) {
    41  	ReturnStatusOK(w)
    42  }
    43  
    44  func authorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
    45  	authRequest := model.AuthorizeRequestFromJson(r.Body)
    46  	if authRequest == nil {
    47  		c.SetInvalidParam("authorize_request")
    48  	}
    49  
    50  	if err := authRequest.IsValid(); err != nil {
    51  		c.Err = err
    52  		return
    53  	}
    54  
    55  	if c.App.Session().IsOAuth {
    56  		c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
    57  		c.Err.DetailedError += ", attempted access by oauth app"
    58  		return
    59  	}
    60  
    61  	auditRec := c.MakeAuditRecord("authorizeOAuthApp", audit.Fail)
    62  	defer c.LogAuditRec(auditRec)
    63  	c.LogAudit("attempt")
    64  
    65  	redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.App.Session().UserId, authRequest)
    66  
    67  	if err != nil {
    68  		c.Err = err
    69  		return
    70  	}
    71  
    72  	auditRec.Success()
    73  	c.LogAudit("")
    74  
    75  	w.Write([]byte(model.MapToJson(map[string]string{"redirect": redirectUrl})))
    76  }
    77  
    78  func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
    79  	requestData := model.MapFromJson(r.Body)
    80  	clientId := requestData["client_id"]
    81  
    82  	if len(clientId) != 26 {
    83  		c.SetInvalidParam("client_id")
    84  		return
    85  	}
    86  
    87  	auditRec := c.MakeAuditRecord("deauthorizeOAuthApp", audit.Fail)
    88  	defer c.LogAuditRec(auditRec)
    89  
    90  	err := c.App.DeauthorizeOAuthAppForUser(c.App.Session().UserId, clientId)
    91  	if err != nil {
    92  		c.Err = err
    93  		return
    94  	}
    95  
    96  	auditRec.Success()
    97  	c.LogAudit("success")
    98  
    99  	ReturnStatusOK(w)
   100  }
   101  
   102  func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) {
   103  	if !*c.App.Config().ServiceSettings.EnableOAuthServiceProvider {
   104  		err := model.NewAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "", http.StatusNotImplemented)
   105  		utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
   106  		return
   107  	}
   108  
   109  	authRequest := &model.AuthorizeRequest{
   110  		ResponseType: r.URL.Query().Get("response_type"),
   111  		ClientId:     r.URL.Query().Get("client_id"),
   112  		RedirectUri:  r.URL.Query().Get("redirect_uri"),
   113  		Scope:        r.URL.Query().Get("scope"),
   114  		State:        r.URL.Query().Get("state"),
   115  	}
   116  
   117  	loginHint := r.URL.Query().Get("login_hint")
   118  
   119  	if err := authRequest.IsValid(); err != nil {
   120  		utils.RenderWebError(c.App.Config(), w, r, err.StatusCode,
   121  			url.Values{
   122  				"type":    []string{"oauth_invalid_param"},
   123  				"message": []string{err.Message},
   124  			}, c.App.AsymmetricSigningKey())
   125  		return
   126  	}
   127  
   128  	oauthApp, err := c.App.GetOAuthApp(authRequest.ClientId)
   129  	if err != nil {
   130  		utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
   131  		return
   132  	}
   133  
   134  	// here we should check if the user is logged in
   135  	if len(c.App.Session().UserId) == 0 {
   136  		if loginHint == model.USER_AUTH_SERVICE_SAML {
   137  			http.Redirect(w, r, c.GetSiteURLHeader()+"/login/sso/saml?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
   138  		} else {
   139  			http.Redirect(w, r, c.GetSiteURLHeader()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
   140  		}
   141  		return
   142  	}
   143  
   144  	if !oauthApp.IsValidRedirectURL(authRequest.RedirectUri) {
   145  		err := model.NewAppError("authorizeOAuthPage", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
   146  		utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
   147  		return
   148  	}
   149  
   150  	isAuthorized := false
   151  
   152  	if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.App.Session().UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, authRequest.ClientId); err == nil {
   153  		// when we support scopes we should check if the scopes match
   154  		isAuthorized = true
   155  	}
   156  
   157  	// Automatically allow if the app is trusted
   158  	if oauthApp.IsTrusted || isAuthorized {
   159  		redirectUrl, err := c.App.AllowOAuthAppAccessToUser(c.App.Session().UserId, authRequest)
   160  
   161  		if err != nil {
   162  			utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
   163  			return
   164  		}
   165  
   166  		http.Redirect(w, r, redirectUrl, http.StatusFound)
   167  		return
   168  	}
   169  
   170  	w.Header().Set("X-Frame-Options", "SAMEORIGIN")
   171  	w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
   172  	w.Header().Set("Content-Type", "text/html; charset=utf-8")
   173  	w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
   174  
   175  	staticDir, _ := fileutils.FindDir(model.CLIENT_DIR)
   176  	http.ServeFile(w, r, filepath.Join(staticDir, "root.html"))
   177  }
   178  
   179  func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
   180  	r.ParseForm()
   181  
   182  	code := r.FormValue("code")
   183  	refreshToken := r.FormValue("refresh_token")
   184  
   185  	grantType := r.FormValue("grant_type")
   186  	switch grantType {
   187  	case model.ACCESS_TOKEN_GRANT_TYPE:
   188  		if len(code) == 0 {
   189  			c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "", http.StatusBadRequest)
   190  			return
   191  		}
   192  	case model.REFRESH_TOKEN_GRANT_TYPE:
   193  		if len(refreshToken) == 0 {
   194  			c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "", http.StatusBadRequest)
   195  			return
   196  		}
   197  	default:
   198  		c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "", http.StatusBadRequest)
   199  		return
   200  	}
   201  
   202  	clientId := r.FormValue("client_id")
   203  	if len(clientId) != 26 {
   204  		c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "", http.StatusBadRequest)
   205  		return
   206  	}
   207  
   208  	secret := r.FormValue("client_secret")
   209  	if len(secret) == 0 {
   210  		c.Err = model.NewAppError("getAccessToken", "api.oauth.get_access_token.bad_client_secret.app_error", nil, "", http.StatusBadRequest)
   211  		return
   212  	}
   213  
   214  	redirectUri := r.FormValue("redirect_uri")
   215  
   216  	auditRec := c.MakeAuditRecord("getAccessToken", audit.Fail)
   217  	defer c.LogAuditRec(auditRec)
   218  	auditRec.AddMeta("grant_type", grantType)
   219  	auditRec.AddMeta("client_id", clientId)
   220  	c.LogAudit("attempt")
   221  
   222  	accessRsp, err := c.App.GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectUri, code, secret, refreshToken)
   223  	if err != nil {
   224  		c.Err = err
   225  		return
   226  	}
   227  
   228  	w.Header().Set("Content-Type", "application/json")
   229  	w.Header().Set("Cache-Control", "no-store")
   230  	w.Header().Set("Pragma", "no-cache")
   231  
   232  	auditRec.Success()
   233  	c.LogAudit("success")
   234  
   235  	w.Write([]byte(accessRsp.ToJson()))
   236  }
   237  
   238  func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
   239  	c.RequireService()
   240  	if c.Err != nil {
   241  		return
   242  	}
   243  
   244  	service := c.Params.Service
   245  
   246  	oauthError := r.URL.Query().Get("error")
   247  	if oauthError == "access_denied" {
   248  		utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
   249  			"type":    []string{"oauth_access_denied"},
   250  			"service": []string{strings.Title(service)},
   251  		}, c.App.AsymmetricSigningKey())
   252  		return
   253  	}
   254  
   255  	code := r.URL.Query().Get("code")
   256  	if len(code) == 0 {
   257  		utils.RenderWebError(c.App.Config(), w, r, http.StatusTemporaryRedirect, url.Values{
   258  			"type":    []string{"oauth_missing_code"},
   259  			"service": []string{strings.Title(service)},
   260  		}, c.App.AsymmetricSigningKey())
   261  		return
   262  	}
   263  
   264  	state := r.URL.Query().Get("state")
   265  
   266  	uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete"
   267  
   268  	body, branchId, props, err := c.App.AuthorizeOAuthUser(w, r, service, code, state, uri)
   269  
   270  	action := ""
   271  	if props != nil {
   272  		action = props["action"]
   273  	}
   274  
   275  	if err != nil {
   276  		err.Translate(c.App.T)
   277  		mlog.Error(err.Error())
   278  		if action == model.OAUTH_ACTION_MOBILE {
   279  			w.Write([]byte(err.ToJson()))
   280  		} else {
   281  			utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
   282  		}
   283  		return
   284  	}
   285  
   286  	user, err := c.App.CompleteOAuth(service, body, branchId, props)
   287  	if err != nil {
   288  		err.Translate(c.App.T)
   289  		mlog.Error(err.Error())
   290  		if action == model.OAUTH_ACTION_MOBILE {
   291  			w.Write([]byte(err.ToJson()))
   292  		} else {
   293  			utils.RenderWebAppError(c.App.Config(), w, r, err, c.App.AsymmetricSigningKey())
   294  		}
   295  		return
   296  	}
   297  
   298  	var redirectUrl string
   299  	if action == model.OAUTH_ACTION_EMAIL_TO_SSO {
   300  		redirectUrl = c.GetSiteURLHeader() + "/login?extra=signin_change"
   301  	} else if action == model.OAUTH_ACTION_SSO_TO_EMAIL {
   302  		redirectUrl = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"])
   303  	} else {
   304  		err = c.App.DoLogin(w, r, user, "")
   305  		if err != nil {
   306  			err.Translate(c.App.T)
   307  			c.Err = err
   308  			if action == model.OAUTH_ACTION_MOBILE {
   309  				w.Write([]byte(err.ToJson()))
   310  			}
   311  			return
   312  		}
   313  
   314  		c.App.AttachSessionCookies(w, r)
   315  
   316  		if _, ok := props["redirect_to"]; ok {
   317  			redirectUrl = props["redirect_to"]
   318  		} else {
   319  			redirectUrl = c.GetSiteURLHeader()
   320  		}
   321  	}
   322  
   323  	if action == model.OAUTH_ACTION_MOBILE {
   324  		return
   325  	}
   326  
   327  	w.Header().Set("Content-Type", "text/html; charset=utf-8")
   328  	http.Redirect(w, r, redirectUrl, http.StatusTemporaryRedirect)
   329  }
   330  
   331  func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
   332  	c.RequireService()
   333  	if c.Err != nil {
   334  		return
   335  	}
   336  
   337  	loginHint := r.URL.Query().Get("login_hint")
   338  	redirectTo := r.URL.Query().Get("redirect_to")
   339  
   340  	branchId, err := c.App.GetBranchIdFromQuery(r.URL.Query())
   341  	if err != nil {
   342  		c.Err = err
   343  		return
   344  	}
   345  
   346  	authUrl, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, branchId, model.OAUTH_ACTION_LOGIN, redirectTo, loginHint)
   347  	if err != nil {
   348  		c.Err = err
   349  		return
   350  	}
   351  
   352  	http.Redirect(w, r, authUrl, http.StatusFound)
   353  }
   354  
   355  func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
   356  	c.RequireService()
   357  	if c.Err != nil {
   358  		return
   359  	}
   360  
   361  	branchId, err := c.App.GetBranchIdFromQuery(r.URL.Query())
   362  	if err != nil {
   363  		c.Err = err
   364  		return
   365  	}
   366  
   367  	authUrl, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, branchId, model.OAUTH_ACTION_MOBILE, "", "")
   368  	if err != nil {
   369  		c.Err = err
   370  		return
   371  	}
   372  
   373  	http.Redirect(w, r, authUrl, http.StatusFound)
   374  }
   375  
   376  func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
   377  	c.RequireService()
   378  	if c.Err != nil {
   379  		return
   380  	}
   381  
   382  	if !*c.App.Config().BranchSettings.EnableUserCreation {
   383  		utils.RenderWebError(c.App.Config(), w, r, http.StatusBadRequest, url.Values{
   384  			"message": []string{utils.T("api.oauth.singup_with_oauth.disabled.app_error")},
   385  		}, c.App.AsymmetricSigningKey())
   386  		return
   387  	}
   388  
   389  	branchId, err := c.App.GetBranchIdFromQuery(r.URL.Query())
   390  	if err != nil {
   391  		c.Err = err
   392  		return
   393  	}
   394  
   395  	authUrl, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, branchId)
   396  	if err != nil {
   397  		c.Err = err
   398  		return
   399  	}
   400  
   401  	http.Redirect(w, r, authUrl, http.StatusFound)
   402  }