Refactor OAuth 2.0 code into app layer (#6037)

This commit is contained in:
Joram Wilander
2017-04-12 16:29:42 -04:00
committed by Harrison Healey
parent 03502cf73b
commit 8b8aa2ca3c
12 changed files with 649 additions and 660 deletions

View File

@@ -4,20 +4,14 @@
package api
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
@@ -46,15 +40,8 @@ func InitOAuth() {
}
func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("registerOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) {
c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
c.Err = model.NewAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
return
}
@@ -65,72 +52,50 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
secret := model.NewId()
oauthApp.ClientSecret = secret
oauthApp.CreatorId = c.Session.UserId
if result := <-app.Srv.Store.OAuth().SaveApp(oauthApp); result.Err != nil {
c.Err = result.Err
return
} else {
oauthApp = result.Data.(*model.OAuthApp)
rapp, err := app.CreateOAuthApp(oauthApp)
c.LogAudit("client_id=" + oauthApp.Id)
w.Write([]byte(oauthApp.ToJson()))
if err != nil {
c.Err = err
return
}
c.LogAudit("client_id=" + rapp.Id)
w.Write([]byte(rapp.ToJson()))
}
func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("getOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) {
c.Err = model.NewLocAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
c.Err = model.NewAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
return
}
var ochan store.StoreChannel
var apps []*model.OAuthApp
var err *model.AppError
if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
ochan = app.Srv.Store.OAuth().GetApps()
apps, err = app.GetOAuthApps(0, 100000)
} else {
c.Err = nil
ochan = app.Srv.Store.OAuth().GetAppByUser(c.Session.UserId)
apps, err = app.GetOAuthAppsByCreator(c.Session.UserId, 0, 100000)
}
if result := <-ochan; result.Err != nil {
c.Err = result.Err
if err != nil {
c.Err = err
return
} else {
apps := result.Data.([]*model.OAuthApp)
w.Write([]byte(model.OAuthAppListToJson(apps)))
}
w.Write([]byte(model.OAuthAppListToJson(apps)))
}
func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
params := mux.Vars(r)
clientId := params["client_id"]
var oauthApp *model.OAuthApp
if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.database.app_error", nil, "")
oauthApp, err := app.GetOAuthApp(clientId)
if err != nil {
c.Err = err
return
} else {
oauthApp = result.Data.(*model.OAuthApp)
}
oauthApp.Sanitize()
@@ -138,123 +103,49 @@ func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
}
func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
c.LogAudit("attempt")
responseData := map[string]string{}
responseType := r.URL.Query().Get("response_type")
if len(responseType) == 0 {
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.bad_response.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
c.Err = model.NewAppError("allowOAuth", "api.oauth.allow_oauth.bad_response.app_error", nil, "", http.StatusBadRequest)
return
}
clientId := r.URL.Query().Get("client_id")
if len(clientId) != 26 {
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.bad_client.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
c.Err = model.NewAppError("allowOAuth", "api.oauth.allow_oauth.bad_client.app_error", nil, "", http.StatusBadRequest)
return
}
redirectUri := r.URL.Query().Get("redirect_uri")
if len(redirectUri) == 0 {
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.bad_redirect.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
c.Err = model.NewAppError("allowOAuth", "api.oauth.allow_oauth.bad_redirect.app_error", nil, "", http.StatusBadRequest)
return
}
scope := r.URL.Query().Get("scope")
state := r.URL.Query().Get("state")
if len(scope) == 0 {
scope = model.DEFAULT_SCOPE
}
c.LogAudit("attempt")
var oauthApp *model.OAuthApp
if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.database.app_error", nil, "")
return
} else {
oauthApp = result.Data.(*model.OAuthApp)
}
redirectUrl, err := app.AllowOAuthAppAccessToUser(c.Session.UserId, responseType, clientId, redirectUri, scope, state)
if !oauthApp.IsValidRedirectURL(redirectUri) {
c.LogAudit("fail - redirect_uri did not match registered callback")
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
if err != nil {
c.Err = err
return
}
if responseType != model.AUTHCODE_RESPONSE_TYPE {
responseData["redirect"] = redirectUri + "?error=unsupported_response_type&state=" + state
w.Write([]byte(model.MapToJson(responseData)))
return
}
c.LogAudit("")
authData := &model.AuthData{UserId: c.Session.UserId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope}
authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId))
// this saves the OAuth2 app as authorized
authorizedApp := model.Preference{
UserId: c.Session.UserId,
Category: model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP,
Name: clientId,
Value: scope,
}
if result := <-app.Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil {
responseData["redirect"] = redirectUri + "?error=server_error&state=" + state
w.Write([]byte(model.MapToJson(responseData)))
return
}
if result := <-app.Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil {
responseData["redirect"] = redirectUri + "?error=server_error&state=" + state
w.Write([]byte(model.MapToJson(responseData)))
return
}
c.LogAudit("success")
responseData["redirect"] = redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State)
w.Write([]byte(model.MapToJson(responseData)))
w.Write([]byte(model.MapToJson(map[string]string{"redirect": redirectUrl})))
}
func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("getAuthorizedApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
apps, err := app.GetAuthorizedAppsForUser(c.Session.UserId, 0, 10000)
if err != nil {
c.Err = err
return
}
ochan := app.Srv.Store.OAuth().GetAuthorizedApps(c.Session.UserId)
if result := <-ochan; result.Err != nil {
c.Err = result.Err
return
} else {
apps := result.Data.([]*model.OAuthApp)
for k, a := range apps {
a.Sanitize()
apps[k] = a
}
w.Write([]byte(model.OAuthAppListToJson(apps)))
}
}
func GetAuthData(code string) *model.AuthData {
if result := <-app.Srv.Store.OAuth().GetAuthData(code); result.Err != nil {
l4g.Error(utils.T("api.oauth.get_auth_data.find.error"), code)
return nil
} else {
return result.Data.(*model.AuthData)
}
w.Write([]byte(model.OAuthAppListToJson(apps)))
}
func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -271,60 +162,36 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
uri := c.GetSiteURLHeader() + "/signup/" + service + "/complete"
if body, teamId, props, err := app.AuthorizeOAuthUser(service, code, state, uri); err != nil {
body, teamId, props, err := app.AuthorizeOAuthUser(service, code, state, uri)
if err != nil {
c.Err = err
return
} else {
defer func() {
ioutil.ReadAll(body)
body.Close()
}()
action := props["action"]
switch action {
case model.OAUTH_ACTION_SIGNUP:
if user, err := app.CreateOAuthUser(service, body, teamId); err != nil {
c.Err = err
} else {
doLogin(c, w, r, user, "")
}
if c.Err == nil {
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_LOGIN:
user := LoginByOAuth(c, w, r, service, body)
if len(teamId) > 0 {
c.Err = app.AddUserToTeamByTeamId(teamId, user)
}
if c.Err == nil {
if val, ok := props["redirect_to"]; ok {
http.Redirect(w, r, c.GetSiteURLHeader()+val, http.StatusTemporaryRedirect)
return
}
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_EMAIL_TO_SSO:
CompleteSwitchWithOAuth(c, w, r, service, body, props["email"])
if c.Err == nil {
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_SSO_TO_EMAIL:
LoginByOAuth(c, w, r, service, body)
if c.Err == nil {
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
}
break
default:
LoginByOAuth(c, w, r, service, body)
if c.Err == nil {
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
}
}
user, err := app.CompleteOAuth(service, body, teamId, props)
if err != nil {
c.Err = err
return
}
action := props["action"]
var redirectUrl string
if action == model.OAUTH_ACTION_EMAIL_TO_SSO {
redirectUrl = c.GetSiteURLHeader() + "/login?extra=signin_change"
} else if action == model.OAUTH_ACTION_SSO_TO_EMAIL {
redirectUrl = app.GetProtocol(r) + "://" + r.Host + "/claim?email=" + url.QueryEscape(props["email"])
} else {
doLogin(c, w, r, user, "")
if c.Err != nil {
return
}
redirectUrl = c.GetSiteURLHeader()
}
http.Redirect(w, r, redirectUrl, http.StatusTemporaryRedirect)
}
func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -371,42 +238,15 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
// Automatically allow if the app is trusted
if oauthApp.IsTrusted || isAuthorized {
closeBody := func(r *http.Response) {
if r.Body != nil {
ioutil.ReadAll(r.Body)
r.Body.Close()
}
}
redirectUrl, err := app.AllowOAuthAppAccessToUser(c.Session.UserId, model.AUTHCODE_RESPONSE_TYPE, clientId, redirect, scope, state)
doAllow := func() (*http.Response, *model.AppError) {
HttpClient := &http.Client{}
url := c.GetSiteURLHeader() + "/api/v3/oauth/allow?response_type=" + model.AUTHCODE_RESPONSE_TYPE + "&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirect) + "&scope=" + scope + "&state=" + url.QueryEscape(state)
rq, _ := http.NewRequest("GET", url, strings.NewReader(""))
rq.Header.Set(model.HEADER_AUTH, model.HEADER_BEARER+" "+c.Session.Token)
if rp, err := HttpClient.Do(rq); err != nil {
return nil, model.NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode == 304 {
return rp, nil
} else if rp.StatusCode >= 300 {
defer closeBody(rp)
return rp, model.AppErrorFromJson(rp.Body)
} else {
return rp, nil
}
}
if result, err := doAllow(); err != nil {
if err != nil {
c.Err = err
return
} else {
//defer closeBody(result)
data := model.MapFromJson(result.Body)
redirectTo := data["redirect"]
http.Redirect(w, r, redirectTo, http.StatusFound)
return
}
http.Redirect(w, r, redirectUrl, http.StatusFound)
return
}
w.Header().Set("Content-Type", "text/html")
@@ -416,14 +256,6 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
c.LogAudit("attempt")
r.ParseForm()
code := r.FormValue("code")
@@ -458,140 +290,21 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
var oauthApp *model.OAuthApp
achan := app.Srv.Store.OAuth().GetApp(clientId)
if result := <-achan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "")
redirectUri := r.FormValue("redirect_uri")
c.LogAudit("attempt")
accessRsp, err := app.GetOAuthAccessToken(clientId, grantType, redirectUri, code, secret, refreshToken)
if err != nil {
c.Err = err
return
} else {
oauthApp = result.Data.(*model.OAuthApp)
}
if oauthApp.ClientSecret != secret {
c.LogAudit("fail - invalid client credentials")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "")
return
}
var user *model.User
var accessData *model.AccessData
var accessRsp *model.AccessResponse
if grantType == model.ACCESS_TOKEN_GRANT_TYPE {
redirectUri := r.FormValue("redirect_uri")
authData := GetAuthData(code)
if authData == nil {
c.LogAudit("fail - invalid auth code")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
return
}
if authData.IsExpired() {
<-app.Srv.Store.OAuth().RemoveAuthData(authData.Code)
c.LogAudit("fail - auth code expired")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
return
}
if authData.RedirectUri != redirectUri {
c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "")
return
}
if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
c.LogAudit("fail - auth code is invalid")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
return
}
uchan := app.Srv.Store.User().Get(authData.UserId)
if result := <-uchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "")
return
} else {
user = result.Data.(*model.User)
}
tchan := app.Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId)
if result := <-tchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "")
return
} else if result.Data != nil {
accessData := result.Data.(*model.AccessData)
if accessData.IsExpired() {
if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil {
c.Err = err
return
} else {
accessRsp = access
}
} else {
//return the same token and no need to create a new session
accessRsp = &model.AccessResponse{
AccessToken: accessData.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
ExpiresIn: int32((accessData.ExpiresAt - model.GetMillis()) / 1000),
}
}
} else {
// create a new session and return new access token
var session *model.Session
if result, err := newSession(oauthApp.Name, user); err != nil {
c.Err = err
return
} else {
session = result
}
accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt}
if result := <-app.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "")
return
}
accessRsp = &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
RefreshToken: accessData.RefreshToken,
ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
}
}
<-app.Srv.Store.OAuth().RemoveAuthData(authData.Code)
} else {
// when grantType is refresh_token
if result := <-app.Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil {
c.LogAudit("fail - refresh token is invalid")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "")
return
} else {
accessData = result.Data.(*model.AccessData)
}
uchan := app.Srv.Store.User().Get(accessData.UserId)
if result := <-uchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "")
return
} else {
user = result.Data.(*model.User)
}
if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil {
c.Err = err
return
} else {
accessRsp = access
}
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
c.LogAuditWithUserId(user.Id, "success")
c.LogAudit("success")
w.Write([]byte(accessRsp.ToJson()))
}
@@ -602,23 +315,13 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
loginHint := r.URL.Query().Get("login_hint")
redirectTo := r.URL.Query().Get("redirect_to")
teamId, err := getTeamIdFromQuery(r.URL.Query())
teamId, err := app.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_LOGIN
if len(teamId) != 0 {
stateProps["team_id"] = teamId
}
if len(redirectTo) != 0 {
stateProps["redirect_to"] = redirectTo
}
if authUrl, err := app.GetAuthorizationCode(service, stateProps, loginHint); err != nil {
if authUrl, err := app.GetOAuthLoginEndpoint(service, teamId, redirectTo, loginHint); err != nil {
c.Err = err
return
} else {
@@ -626,59 +329,22 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func getTeamIdFromQuery(query url.Values) (string, *model.AppError) {
hash := query.Get("h")
inviteId := query.Get("id")
if len(hash) > 0 {
data := query.Get("d")
props := model.MapFromJson(strings.NewReader(data))
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "")
}
t, err := strconv.ParseInt(props["time"], 10, 64)
if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "")
}
return props["id"], nil
} else if len(inviteId) > 0 {
if result := <-app.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 {
return result.Data.(*model.Team).Id, nil
}
}
return "", nil
}
func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
if !utils.Cfg.TeamSettings.EnableUserCreation {
c.Err = model.NewLocAppError("signupWithOAuth", "api.oauth.singup_with_oauth.disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
c.Err = model.NewAppError("signupWithOAuth", "api.oauth.singup_with_oauth.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
teamId, err := getTeamIdFromQuery(r.URL.Query())
teamId, err := app.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return
}
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_SIGNUP
if len(teamId) != 0 {
stateProps["team_id"] = teamId
}
if authUrl, err := app.GetAuthorizationCode(service, stateProps, ""); err != nil {
if authUrl, err := app.GetOAuthSignupEndpoint(service, teamId); err != nil {
c.Err = err
return
} else {
@@ -686,75 +352,7 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
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": strings.Title(service)}, "")
return
} else {
ssoUser := provider.GetUserFromJson(userData)
ssoEmail = ssoUser.Email
if ssoUser.AuthData != nil {
authData = *ssoUser.AuthData
}
}
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 := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil {
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
}
if err := app.RevokeAllSessions(user.Id); err != nil {
c.Err = err
return
}
c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil {
c.Err = result.Err
return
}
go func() {
if err := app.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, utils.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
}()
}
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) {
c.Err = model.NewLocAppError("deleteOAuthApp", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
c.LogAudit("attempt")
props := model.MapFromJson(r.Body)
id := props["id"]
@@ -763,18 +361,27 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil {
c.Err = result.Err
c.LogAudit("attempt")
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) {
c.Err = model.NewAppError("deleteOAuthApp", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
return
} else {
if c.Session.UserId != result.Data.(*model.OAuthApp).CreatorId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.delete.permissions.app_error", nil, "user_id="+c.Session.UserId)
return
}
}
if err := (<-app.Srv.Store.OAuth().DeleteApp(id)).Err; err != nil {
oauthApp, err := app.GetOAuthApp(id)
if err != nil {
c.Err = err
return
}
if c.Session.UserId != oauthApp.CreatorId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewAppError("deleteOAuthApp", "api.oauth.delete.permissions.app_error", nil, "user_id="+c.Session.UserId, http.StatusForbidden)
return
}
err = app.DeleteOAuthApp(id)
if err != nil {
c.Err = err
return
}
@@ -784,37 +391,11 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
}
func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
params := mux.Vars(r)
id := params["id"]
// revoke app sessions
if result := <-app.Srv.Store.OAuth().GetAccessDataByUserForApp(c.Session.UserId, id); result.Err != nil {
c.Err = result.Err
return
} else {
accessData := result.Data.([]*model.AccessData)
for _, a := range accessData {
if err := app.RevokeAccessToken(a.Token); err != nil {
c.Err = err
return
}
if rad := <-app.Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil {
c.Err = rad.Err
return
}
}
}
// Deauthorize the app
if err := (<-app.Srv.Store.Preference().Delete(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, id)).Err; err != nil {
err := app.DeauthorizeOAuthAppForUser(c.Session.UserId, id)
if err != nil {
c.Err = err
return
}
@@ -824,78 +405,25 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
}
func regenerateOAuthSecret(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("registerOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
params := mux.Vars(r)
id := params["id"]
var oauthApp *model.OAuthApp
if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil {
c.Err = model.NewLocAppError("regenerateOAuthSecret", "api.oauth.allow_oauth.database.app_error", nil, "")
return
} else {
oauthApp = result.Data.(*model.OAuthApp)
if oauthApp.CreatorId != c.Session.UserId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
oauthApp.ClientSecret = model.NewId()
if update := <-app.Srv.Store.OAuth().UpdateApp(oauthApp); update.Err != nil {
c.Err = update.Err
return
}
w.Write([]byte(oauthApp.ToJson()))
oauthApp, err := app.GetOAuthApp(id)
if err != nil {
c.Err = err
return
}
}
func newSession(appName string, user *model.User) (*model.Session, *model.AppError) {
// set new token an session
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays)
session.AddProp(model.SESSION_PROP_PLATFORM, appName)
session.AddProp(model.SESSION_PROP_OS, "OAuth2")
session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2")
if result := <-app.Srv.Store.Session().Save(session); result.Err != nil {
return nil, model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_session.app_error", nil, "")
} else {
session = result.Data.(*model.Session)
app.AddSessionToCache(session)
}
return session, nil
}
func newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) {
var session *model.Session
<-app.Srv.Store.Session().Remove(accessData.Token) //remove the previous session
if result, err := newSession(appName, user); err != nil {
return nil, err
} else {
session = result
}
accessData.Token = session.Token
accessData.ExpiresAt = session.ExpiresAt
if result := <-app.Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
return nil, model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
}
accessRsp := &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
}
return accessRsp, nil
if oauthApp.CreatorId != c.Session.UserId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
c.Err = model.NewAppError("regenerateOAuthSecret", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
return
}
oauthApp, err = app.RegenerateOAuthAppSecret(oauthApp)
if err != nil {
c.Err = err
return
}
w.Write([]byte(oauthApp.ToJson()))
}

View File

@@ -491,7 +491,7 @@ func TestOAuthAuthorize(t *testing.T) {
}
authToken := Client.AuthType + " " + Client.AuthToken
if r, err := HttpGet(Client.Url+"/oauth/authorize?client_id="+oauthApp.Id+"&&redirect_uri=http://example.com&response_type="+model.AUTHCODE_RESPONSE_TYPE, Client.HttpClient, authToken, true); err != nil {
if r, err := HttpGet(Client.Url+"/oauth/authorize?client_id="+oauthApp.Id+"&redirect_uri=http://example.com&response_type="+model.AUTHCODE_RESPONSE_TYPE, Client.HttpClient, authToken, true); err != nil {
t.Fatal(err)
closeBody(r)
}

View File

@@ -4,10 +4,8 @@
package api
import (
"bytes"
b64 "encoding/base64"
"fmt"
"io"
"net/http"
"strconv"
"strings"
@@ -132,52 +130,6 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(user.ToJson()))
}
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader) *model.User {
buf := bytes.Buffer{}
buf.ReadFrom(userData)
authData := ""
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
c.Err = model.NewLocAppError("LoginByOAuth", "api.user.login_by_oauth.not_available.app_error",
map[string]interface{}{"Service": strings.Title(service)}, "")
return nil
} else {
authData = provider.GetAuthDataFromJson(bytes.NewReader(buf.Bytes()))
}
if len(authData) == 0 {
c.Err = model.NewLocAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error",
map[string]interface{}{"Service": service}, "")
return nil
}
var user *model.User
var err *model.AppError
if user, err = app.GetUserByAuth(&authData, service); err != nil {
if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR {
if user, err = app.CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), ""); err != nil {
c.Err = err
return nil
}
}
c.Err = err
return nil
}
if err = app.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service); err != nil {
c.Err = err
return nil
}
doLogin(c, w, r, user, "")
if c.Err != nil {
return nil
}
return user
}
// User MUST be authenticated completely before calling Login
func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
session, err := app.DoLogin(w, r, user, deviceId)
@@ -1188,7 +1140,7 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
teamId, err := getTeamIdFromQuery(r.URL.Query())
teamId, err := app.GetTeamIdFromQuery(r.URL.Query())
if err != nil {
c.Err = err
return

View File

@@ -4,8 +4,10 @@
package app
import (
"bytes"
"crypto/tls"
b64 "encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
@@ -13,10 +15,371 @@ import (
"strings"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
func CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("CreateOAuthApp", "api.oauth.register_oauth_app.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
secret := model.NewId()
app.ClientSecret = secret
if result := <-Srv.Store.OAuth().SaveApp(app); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.OAuthApp), nil
}
}
func GetOAuthApp(appId string) (*model.OAuthApp, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if result := <-Srv.Store.OAuth().GetApp(appId); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.OAuthApp), nil
}
}
func DeleteOAuthApp(appId string) *model.AppError {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if err := (<-Srv.Store.OAuth().DeleteApp(appId)).Err; err != nil {
return err
}
return nil
}
func GetOAuthApps(page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if result := <-Srv.Store.OAuth().GetApps(page*perPage, perPage); result.Err != nil {
return nil, result.Err
} else {
return result.Data.([]*model.OAuthApp), nil
}
}
func GetOAuthAppsByCreator(userId string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if result := <-Srv.Store.OAuth().GetAppByUser(userId, page*perPage, perPage); result.Err != nil {
return nil, result.Err
} else {
return result.Data.([]*model.OAuthApp), nil
}
}
func AllowOAuthAppAccessToUser(userId, responseType, clientId, redirectUri, scope, state string) (string, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if len(scope) == 0 {
scope = model.DEFAULT_SCOPE
}
var oauthApp *model.OAuthApp
if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
return "", result.Err
} else {
oauthApp = result.Data.(*model.OAuthApp)
}
if !oauthApp.IsValidRedirectURL(redirectUri) {
return "", model.NewAppError("AllowOAuthAppAccessToUser", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "", http.StatusBadRequest)
}
if responseType != model.AUTHCODE_RESPONSE_TYPE {
return redirectUri + "?error=unsupported_response_type&state=" + state, nil
}
authData := &model.AuthData{UserId: userId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope}
authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, userId))
// this saves the OAuth2 app as authorized
authorizedApp := model.Preference{
UserId: userId,
Category: model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP,
Name: clientId,
Value: scope,
}
if result := <-Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil {
return redirectUri + "?error=server_error&state=" + state, nil
}
if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil {
return redirectUri + "?error=server_error&state=" + state, nil
}
return redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State), nil
}
func GetOAuthAccessToken(clientId, grantType, redirectUri, code, secret, refreshToken string) (*model.AccessResponse, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "", http.StatusNotImplemented)
}
var oauthApp *model.OAuthApp
if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusNotFound)
} else {
oauthApp = result.Data.(*model.OAuthApp)
}
if oauthApp.ClientSecret != secret {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "", http.StatusForbidden)
}
var user *model.User
var accessData *model.AccessData
var accessRsp *model.AccessResponse
if grantType == model.ACCESS_TOKEN_GRANT_TYPE {
var authData *model.AuthData
if result := <-Srv.Store.OAuth().GetAuthData(code); result.Err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusInternalServerError)
} else {
authData = result.Data.(*model.AuthData)
}
if authData.IsExpired() {
<-Srv.Store.OAuth().RemoveAuthData(authData.Code)
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusForbidden)
}
if authData.RedirectUri != redirectUri {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "", http.StatusBadRequest)
}
if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusBadRequest)
}
if result := <-Srv.Store.User().Get(authData.UserId); result.Err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
} else {
user = result.Data.(*model.User)
}
if result := <-Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId); result.Err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "", http.StatusInternalServerError)
} else if result.Data != nil {
accessData := result.Data.(*model.AccessData)
if accessData.IsExpired() {
if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil {
return nil, err
} else {
accessRsp = access
}
} else {
//return the same token and no need to create a new session
accessRsp = &model.AccessResponse{
AccessToken: accessData.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
ExpiresIn: int32((accessData.ExpiresAt - model.GetMillis()) / 1000),
}
}
} else {
// create a new session and return new access token
var session *model.Session
if result, err := newSession(oauthApp.Name, user); err != nil {
return nil, err
} else {
session = result
}
accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt, Scope: authData.Scope}
if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp = &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
RefreshToken: accessData.RefreshToken,
ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
}
}
<-Srv.Store.OAuth().RemoveAuthData(authData.Code)
} else {
// when grantType is refresh_token
if result := <-Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "", http.StatusNotFound)
} else {
accessData = result.Data.(*model.AccessData)
}
if result := <-Srv.Store.User().Get(accessData.UserId); result.Err != nil {
return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound)
} else {
user = result.Data.(*model.User)
}
if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil {
return nil, err
} else {
accessRsp = access
}
}
return accessRsp, nil
}
func newSession(appName string, user *model.User) (*model.Session, *model.AppError) {
// set new token an session
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays)
session.AddProp(model.SESSION_PROP_PLATFORM, appName)
session.AddProp(model.SESSION_PROP_OS, "OAuth2")
session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2")
if result := <-Srv.Store.Session().Save(session); result.Err != nil {
return nil, model.NewAppError("newSession", "api.oauth.get_access_token.internal_session.app_error", nil, "", http.StatusInternalServerError)
} else {
session = result.Data.(*model.Session)
AddSessionToCache(session)
}
return session, nil
}
func newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) {
var session *model.Session
<-Srv.Store.Session().Remove(accessData.Token) //remove the previous session
if result, err := newSession(appName, user); err != nil {
return nil, err
} else {
session = result
}
accessData.Token = session.Token
accessData.ExpiresAt = session.ExpiresAt
if result := <-Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
return nil, model.NewAppError("newSessionUpdateToken", "web.get_access_token.internal_saving.app_error", nil, "", http.StatusInternalServerError)
}
accessRsp := &model.AccessResponse{
AccessToken: session.Token,
TokenType: model.ACCESS_TOKEN_TYPE,
ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
}
return accessRsp, nil
}
func GetOAuthLoginEndpoint(service, teamId, redirectTo, loginHint string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_LOGIN
if len(teamId) != 0 {
stateProps["team_id"] = teamId
}
if len(redirectTo) != 0 {
stateProps["redirect_to"] = redirectTo
}
if authUrl, err := GetAuthorizationCode(service, stateProps, loginHint); err != nil {
return "", err
} else {
return authUrl, nil
}
}
func GetOAuthSignupEndpoint(service, teamId string) (string, *model.AppError) {
stateProps := map[string]string{}
stateProps["action"] = model.OAUTH_ACTION_SIGNUP
if len(teamId) != 0 {
stateProps["team_id"] = teamId
}
if authUrl, err := GetAuthorizationCode(service, stateProps, ""); err != nil {
return "", err
} else {
return authUrl, nil
}
}
func GetAuthorizedAppsForUser(userId string, page, perPage int) ([]*model.OAuthApp, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("GetAuthorizedAppsForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
if result := <-Srv.Store.OAuth().GetAuthorizedApps(userId, page*perPage, perPage); result.Err != nil {
return nil, result.Err
} else {
apps := result.Data.([]*model.OAuthApp)
for k, a := range apps {
a.Sanitize()
apps[k] = a
}
return apps, nil
}
}
func DeauthorizeOAuthAppForUser(userId, appId string) *model.AppError {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return model.NewAppError("DeauthorizeOAuthAppForUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
// revoke app sessions
if result := <-Srv.Store.OAuth().GetAccessDataByUserForApp(userId, appId); result.Err != nil {
return result.Err
} else {
accessData := result.Data.([]*model.AccessData)
for _, a := range accessData {
if err := RevokeAccessToken(a.Token); err != nil {
return err
}
if rad := <-Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil {
return rad.Err
}
}
}
// Deauthorize the app
if err := (<-Srv.Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, appId)).Err; err != nil {
return err
}
return nil
}
func RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
return nil, model.NewAppError("RegenerateOAuthAppSecret", "api.oauth.allow_oauth.turn_off.app_error", nil, "", http.StatusNotImplemented)
}
app.ClientSecret = model.NewId()
if update := <-Srv.Store.OAuth().UpdateApp(app); update.Err != nil {
return nil, update.Err
}
return app, nil
}
func RevokeAccessToken(token string) *model.AppError {
session, _ := GetSession(token)
schan := Srv.Store.Session().Remove(token)
@@ -42,10 +405,122 @@ func RevokeAccessToken(token string) *model.AppError {
return nil
}
func CompleteOAuth(service string, body io.ReadCloser, teamId string, props map[string]string) (*model.User, *model.AppError) {
defer func() {
ioutil.ReadAll(body)
body.Close()
}()
action := props["action"]
switch action {
case model.OAUTH_ACTION_SIGNUP:
return CreateOAuthUser(service, body, teamId)
case model.OAUTH_ACTION_LOGIN:
return LoginByOAuth(service, body, teamId)
case model.OAUTH_ACTION_EMAIL_TO_SSO:
return CompleteSwitchWithOAuth(service, body, props["email"])
case model.OAUTH_ACTION_SSO_TO_EMAIL:
return LoginByOAuth(service, body, teamId)
default:
return LoginByOAuth(service, body, teamId)
}
}
func LoginByOAuth(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) {
buf := bytes.Buffer{}
buf.ReadFrom(userData)
authData := ""
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.not_available.app_error",
map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
} else {
authData = provider.GetAuthDataFromJson(bytes.NewReader(buf.Bytes()))
}
if len(authData) == 0 {
return nil, model.NewAppError("LoginByOAuth", "api.user.login_by_oauth.parse.app_error",
map[string]interface{}{"Service": service}, "", http.StatusBadRequest)
}
user, err := GetUserByAuth(&authData, service)
if err != nil {
if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR {
return CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), teamId)
}
return nil, err
}
if err = UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service); err != nil {
return nil, err
}
if len(teamId) > 0 {
err = AddUserToTeamByTeamId(teamId, user)
}
if err != nil {
return nil, err
}
return user, nil
}
func CompleteSwitchWithOAuth(service string, userData io.ReadCloser, email string) (*model.User, *model.AppError) {
authData := ""
ssoEmail := ""
provider := einterfaces.GetOauthProvider(service)
if provider == nil {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.unavailable.app_error",
map[string]interface{}{"Service": strings.Title(service)}, "", http.StatusNotImplemented)
} else {
ssoUser := provider.GetUserFromJson(userData)
ssoEmail = ssoUser.Email
if ssoUser.AuthData != nil {
authData = *ssoUser.AuthData
}
}
if len(authData) == 0 {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.parse.app_error",
map[string]interface{}{"Service": service}, "", http.StatusBadRequest)
}
if len(email) == 0 {
return nil, model.NewAppError("CompleteSwitchWithOAuth", "api.user.complete_switch_with_oauth.blank_email.app_error", nil, "", http.StatusBadRequest)
}
var user *model.User
if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
return nil, result.Err
} else {
user = result.Data.(*model.User)
}
if err := RevokeAllSessions(user.Id); err != nil {
return nil, err
}
if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil {
return nil, result.Err
}
go func() {
if err := SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, utils.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
}()
return user, nil
}
func GetAuthorizationCode(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)
return "", model.NewAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented)
}
clientId := sso.Id

View File

@@ -6,6 +6,7 @@ package app
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
@@ -747,3 +748,33 @@ func GetTeamStats(teamId string) (*model.TeamStats, *model.AppError) {
return stats, nil
}
func GetTeamIdFromQuery(query url.Values) (string, *model.AppError) {
hash := query.Get("h")
inviteId := query.Get("id")
if len(hash) > 0 {
data := query.Get("d")
props := model.MapFromJson(strings.NewReader(data))
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "", http.StatusBadRequest)
}
t, err := strconv.ParseInt(props["time"], 10, 64)
if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
return "", model.NewAppError("GetTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "", http.StatusBadRequest)
}
return props["id"], nil
} 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 {
return result.Data.(*model.Team).Id, nil
}
}
return "", nil
}

View File

@@ -21,6 +21,7 @@ type AccessData struct {
RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"`
ExpiresAt int64 `json:"expires_at"`
Scope string `json:"scope"`
}
type AccessResponse struct {

View File

@@ -4,6 +4,7 @@
package store
import (
"net/http"
"strings"
"github.com/go-gorp/gorp"
@@ -42,6 +43,7 @@ func NewSqlOAuthStore(sqlStore *SqlStore) OAuthStore {
tableAccess.ColMap("Token").SetMaxSize(26)
tableAccess.ColMap("RefreshToken").SetMaxSize(26)
tableAccess.ColMap("RedirectUri").SetMaxSize(256)
tableAccess.ColMap("Scope").SetMaxSize(128)
tableAccess.SetUniqueTogether("ClientId", "UserId")
}
@@ -138,9 +140,9 @@ func (as SqlOAuthStore) GetApp(id string) StoreChannel {
result := StoreResult{}
if obj, err := as.GetReplica().Get(model.OAuthApp{}, id); err != nil {
result.Err = model.NewLocAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.finding.app_error", nil, "app_id="+id+", "+err.Error())
result.Err = model.NewAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.finding.app_error", nil, "app_id="+id+", "+err.Error(), http.StatusInternalServerError)
} else if obj == nil {
result.Err = model.NewLocAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.find.app_error", nil, "app_id="+id)
result.Err = model.NewAppError("SqlOAuthStore.GetApp", "store.sql_oauth.get_app.find.app_error", nil, "app_id="+id, http.StatusNotFound)
} else {
result.Data = obj.(*model.OAuthApp)
}
@@ -153,7 +155,7 @@ func (as SqlOAuthStore) GetApp(id string) StoreChannel {
return storeChannel
}
func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel {
func (as SqlOAuthStore) GetAppByUser(userId string, offset, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -162,8 +164,8 @@ func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel {
var apps []*model.OAuthApp
if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = :UserId", map[string]interface{}{"UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_app_by_user.find.app_error", nil, "user_id="+userId+", "+err.Error())
if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = :UserId LIMIT :Limit OFFSET :Offset", map[string]interface{}{"UserId": userId, "Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_app_by_user.find.app_error", nil, "user_id="+userId+", "+err.Error(), http.StatusInternalServerError)
}
result.Data = apps
@@ -175,7 +177,7 @@ func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel {
return storeChannel
}
func (as SqlOAuthStore) GetApps() StoreChannel {
func (as SqlOAuthStore) GetApps(offset, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -184,8 +186,8 @@ func (as SqlOAuthStore) GetApps() StoreChannel {
var apps []*model.OAuthApp
if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps"); err != nil {
result.Err = model.NewLocAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error())
if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps LIMIT :Limit OFFSET :Offset", map[string]interface{}{"Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
}
result.Data = apps
@@ -197,7 +199,7 @@ func (as SqlOAuthStore) GetApps() StoreChannel {
return storeChannel
}
func (as SqlOAuthStore) GetAuthorizedApps(userId string) StoreChannel {
func (as SqlOAuthStore) GetAuthorizedApps(userId string, offset, limit int) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
@@ -207,8 +209,8 @@ func (as SqlOAuthStore) GetAuthorizedApps(userId string) StoreChannel {
if _, err := as.GetReplica().Select(&apps,
`SELECT o.* FROM OAuthApps AS o INNER JOIN
Preferences AS p ON p.Name=o.Id AND p.UserId=:UserId`, map[string]interface{}{"UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlOAuthStore.GetAuthorizedApps", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error())
Preferences AS p ON p.Name=o.Id AND p.UserId=:UserId LIMIT :Limit OFFSET :Offset`, map[string]interface{}{"UserId": userId, "Offset": offset, "Limit": limit}); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAuthorizedApps", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
}
result.Data = apps

View File

@@ -56,7 +56,7 @@ func TestOAuthStoreGetApp(t *testing.T) {
}
// Lets try and get the app from a user that hasn't created any apps
if result := (<-store.OAuth().GetAppByUser("fake0123456789abcderfgret1")); result.Err == nil {
if result := (<-store.OAuth().GetAppByUser("fake0123456789abcderfgret1", 0, 1000)); result.Err == nil {
if len(result.Data.([]*model.OAuthApp)) > 0 {
t.Fatal("Should have failed. Fake user hasn't created any apps")
}
@@ -64,11 +64,11 @@ func TestOAuthStoreGetApp(t *testing.T) {
t.Fatal(result.Err)
}
if err := (<-store.OAuth().GetAppByUser(a1.CreatorId)).Err; err != nil {
if err := (<-store.OAuth().GetAppByUser(a1.CreatorId, 0, 1000)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.OAuth().GetApps()).Err; err != nil {
if err := (<-store.OAuth().GetApps(0, 1000)).Err; err != nil {
t.Fatal(err)
}
}
@@ -324,7 +324,7 @@ func TestOAuthGetAuthorizedApps(t *testing.T) {
Must(store.OAuth().SaveApp(&a1))
// Lets try and get an Authorized app for a user who hasn't authorized it
if result := <-store.OAuth().GetAuthorizedApps("fake0123456789abcderfgret1"); result.Err == nil {
if result := <-store.OAuth().GetAuthorizedApps("fake0123456789abcderfgret1", 0, 1000); result.Err == nil {
if len(result.Data.([]*model.OAuthApp)) > 0 {
t.Fatal("Should have failed. Fake user hasn't authorized the app")
}
@@ -340,7 +340,7 @@ func TestOAuthGetAuthorizedApps(t *testing.T) {
p.Value = "true"
Must(store.Preference().Save(&model.Preferences{p}))
if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId); result.Err != nil {
if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000); result.Err != nil {
t.Fatal(result.Err)
} else {
apps := result.Data.([]*model.OAuthApp)
@@ -368,7 +368,7 @@ func TestOAuthGetAccessDataByUserForApp(t *testing.T) {
p.Value = "true"
Must(store.Preference().Save(&model.Preferences{p}))
if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId); result.Err != nil {
if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId, 0, 1000); result.Err != nil {
t.Fatal(result.Err)
} else {
apps := result.Data.([]*model.OAuthApp)

View File

@@ -257,6 +257,7 @@ func UpgradeDatabaseToVersion38(sqlStore *SqlStore) {
func UpgradeDatabaseToVersion39(sqlStore *SqlStore) {
// TODO: Uncomment following condition when version 3.9.0 is released
//if shouldPerformUpgrade(sqlStore, VERSION_3_8_0, VERSION_3_9_0) {
sqlStore.CreateColumnIfNotExists("OAuthAccessData", "Scope", "varchar(128)", "varchar(128)", model.DEFAULT_SCOPE)
// saveSchemaVersion(sqlStore, VERSION_3_9_0)
//}

View File

@@ -389,8 +389,7 @@ func (us SqlUserStore) Get(id string) StoreChannel {
if obj, err := us.GetReplica().Get(model.User{}, id); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.Get", "store.sql_user.get.app_error", nil, "user_id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewLocAppError("SqlUserStore.Get", MISSING_ACCOUNT_ERROR, nil, "user_id="+id)
result.Err.StatusCode = http.StatusNotFound
result.Err = model.NewAppError("SqlUserStore.Get", MISSING_ACCOUNT_ERROR, nil, "user_id="+id, http.StatusNotFound)
} else {
result.Data = obj.(*model.User)
}

View File

@@ -246,9 +246,9 @@ type OAuthStore interface {
SaveApp(app *model.OAuthApp) StoreChannel
UpdateApp(app *model.OAuthApp) StoreChannel
GetApp(id string) StoreChannel
GetAppByUser(userId string) StoreChannel
GetApps() StoreChannel
GetAuthorizedApps(userId string) StoreChannel
GetAppByUser(userId string, offset, limit int) StoreChannel
GetApps(offset, limit int) StoreChannel
GetAuthorizedApps(userId string, offset, limit int) StoreChannel
DeleteApp(id string) StoreChannel
SaveAuthData(authData *model.AuthData) StoreChannel
GetAuthData(code string) StoreChannel

View File

@@ -12,7 +12,7 @@ describe('Client.OAuth', function() {
app.name = 'test';
app.homepage = 'homepage';
app.description = 'desc';
app.callback_urls = '';
app.callback_urls = [''];
TestHelper.basicClient().registerOAuthApp(
app,
@@ -33,7 +33,7 @@ describe('Client.OAuth', function() {
TestHelper.basicClient().allowOAuth2(
'GET',
'123456',
'12345678901234567890123456',
'http://nowhere.com',
'state',
'scope',