Implement OAuth2 service provider functionality.

Этот коммит содержится в:
JoramWilander
2015-09-16 15:49:12 -04:00
родитель 7e418714bc
Коммит 47e6a33a45
45 изменённых файлов: 2506 добавлений и 199 удалений

72
web/react/components/authorize.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,72 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../utils/client.jsx');
export default class Authorize extends React.Component {
constructor(props) {
super(props);
this.handleAllow = this.handleAllow.bind(this);
this.handleDeny = this.handleDeny.bind(this);
this.state = {};
}
handleAllow() {
const responseType = this.props.responseType;
const clientId = this.props.clientId;
const redirectUri = this.props.redirectUri;
const state = this.props.state;
const scope = this.props.scope;
Client.allowOAuth2(responseType, clientId, redirectUri, state, scope,
(data) => {
if (data.redirect) {
window.location.replace(data.redirect);
}
},
() => {}
);
}
handleDeny() {
window.location.replace(this.props.redirectUri + '?error=access_denied');
}
render() {
return (
<div className='authorize-box'>
<div className='authorize-inner'>
<h3>{'An application would like to connect to your '}{this.props.teamName}{' account'}</h3>
<label>{'The app '}{this.props.appName}{' would like the ability to access and modify your basic information.'}</label>
<br/>
<br/>
<label>{'Allow '}{this.props.appName}{' access?'}</label>
<br/>
<button
type='submit'
className='btn authorize-btn'
onClick={this.handleDeny}
>
{'Deny'}
</button>
<button
type='submit'
className='btn btn-primary authorize-btn'
onClick={this.handleAllow}
>
{'Allow'}
</button>
</div>
</div>
);
}
}
Authorize.propTypes = {
appName: React.PropTypes.string,
teamName: React.PropTypes.string,
responseType: React.PropTypes.string,
clientId: React.PropTypes.string,
redirectUri: React.PropTypes.string,
state: React.PropTypes.string,
scope: React.PropTypes.string
};

Просмотреть файл

@@ -25,7 +25,7 @@ export default class PopoverListMembers extends React.Component {
$('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true});
$('body').on('click', function onClick(e) {
if ($(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
if (e.target.parentNode && $(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
$('#member_popover').popover('hide');
}
});

249
web/react/components/register_app_modal.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,249 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../utils/client.jsx');
export default class RegisterAppModal extends React.Component {
constructor() {
super();
this.register = this.register.bind(this);
this.onHide = this.onHide.bind(this);
this.save = this.save.bind(this);
this.state = {clientId: '', clientSecret: '', saved: false};
}
componentDidMount() {
$(React.findDOMNode(this)).on('hide.bs.modal', this.onHide);
}
register() {
var state = this.state;
state.serverError = null;
var app = {};
var name = this.refs.name.getDOMNode().value;
if (!name || name.length === 0) {
state.nameError = 'Application name must be filled in.';
this.setState(state);
return;
}
state.nameError = null;
app.name = name;
var homepage = this.refs.homepage.getDOMNode().value;
if (!homepage || homepage.length === 0) {
state.homepageError = 'Homepage must be filled in.';
this.setState(state);
return;
}
state.homepageError = null;
app.homepage = homepage;
var desc = this.refs.desc.getDOMNode().value;
app.description = desc;
var rawCallbacks = this.refs.callback.getDOMNode().value.trim();
if (!rawCallbacks || rawCallbacks.length === 0) {
state.callbackError = 'At least one callback URL must be filled in.';
this.setState(state);
return;
}
state.callbackError = null;
app.callback_urls = rawCallbacks.split('\n');
Client.registerOAuthApp(app,
(data) => {
state.clientId = data.id;
state.clientSecret = data.client_secret;
this.setState(state);
},
(err) => {
state.serverError = err.message;
this.setState(state);
}
);
}
onHide(e) {
if (!this.state.saved && this.state.clientId !== '') {
e.preventDefault();
return;
}
this.setState({clientId: '', clientSecret: '', saved: false});
}
save() {
this.setState({saved: this.refs.save.getDOMNode().checked});
}
render() {
var nameError;
if (this.state.nameError) {
nameError = <div className='form-group has-error'><label className='control-label'>{this.state.nameError}</label></div>;
}
var homepageError;
if (this.state.homepageError) {
homepageError = <div className='form-group has-error'><label className='control-label'>{this.state.homepageError}</label></div>;
}
var callbackError;
if (this.state.callbackError) {
callbackError = <div className='form-group has-error'><label className='control-label'>{this.state.callbackError}</label></div>;
}
var serverError;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var body = '';
if (this.state.clientId === '') {
body = (
<div className='form-group user-settings'>
<h3>{'Register a New Application'}</h3>
<br/>
<label className='col-sm-4 control-label'>{'Application Name'}</label>
<div className='col-sm-7'>
<input
ref='name'
className='form-control'
type='text'
placeholder='Required'
/>
{nameError}
</div>
<br/>
<br/>
<label className='col-sm-4 control-label'>{'Homepage URL'}</label>
<div className='col-sm-7'>
<input
ref='homepage'
className='form-control'
type='text'
placeholder='Required'
/>
{homepageError}
</div>
<br/>
<br/>
<label className='col-sm-4 control-label'>{'Description'}</label>
<div className='col-sm-7'>
<input
ref='desc'
className='form-control'
type='text'
placeholder='Optional'
/>
</div>
<br/>
<br/>
<label className='col-sm-4 control-label'>{'Callback URL'}</label>
<div className='col-sm-7'>
<textarea
ref='callback'
className='form-control'
type='text'
placeholder='Required'
rows='5'
/>
{callbackError}
</div>
<br/>
<br/>
<br/>
<br/>
<br/>
{serverError}
<a
className='btn btn-sm theme pull-right'
href='#'
data-dismiss='modal'
aria-label='Close'
>
{'Cancel'}
</a>
<a
className='btn btn-sm btn-primary pull-right'
onClick={this.register}
>
{'Register'}
</a>
</div>
);
} else {
var btnClass = ' disabled';
if (this.state.saved) {
btnClass = '';
}
body = (
<div className='form-group user-settings'>
<h3>{'Your Application Credentials'}</h3>
<br/>
<br/>
<label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label>
<label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label>
<br/>
<br/>
<br/>
<br/>
<strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong>
<br/>
<br/>
<div className='checkbox'>
<label>
<input
ref='save'
type='checkbox'
checked={this.state.saved}
onClick={this.save}
>
{'I have saved both my Client Id and Client Secret somewhere safe'}
</input>
</label>
</div>
<a
className={'btn btn-sm btn-primary pull-right' + btnClass}
href='#'
data-dismiss='modal'
aria-label='Close'
>
{'Close'}
</a>
</div>
);
}
return (
<div
className='modal fade'
ref='modal'
id='register_app'
role='dialog'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>{'x'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
{'Developer Applications'}
</h4>
</div>
<div className='modal-body'>
{body}
</div>
</div>
</div>
</div>
);
}
}

Просмотреть файл

@@ -7,6 +7,7 @@ var NotificationsTab = require('./user_settings_notifications.jsx');
var SecurityTab = require('./user_settings_security.jsx');
var GeneralTab = require('./user_settings_general.jsx');
var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -76,6 +77,15 @@ export default class UserSettings extends React.Component {
/>
</div>
);
} else if (this.props.activeTab === 'developer') {
return (
<div>
<DeveloperTab
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
/>
</div>
);
}
return <div/>;

Просмотреть файл

@@ -0,0 +1,93 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx');
export default class DeveloperTab extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
register() {
$('#user_settings1').modal('hide');
$('#register_app').modal('show');
}
render() {
var appSection;
var self = this;
if (this.props.activeSection === 'app') {
var inputs = [];
inputs.push(
<div className='form-group'>
<div className='col-sm-7'>
<a
className='btn btn-sm btn-primary'
onClick={this.register}
>
{'Register New Application'}
</a>
</div>
</div>
);
appSection = (
<SettingItemMax
title='Applications (Preview)'
inputs={inputs}
updateSection={function updateSection(e) {
self.props.updateSection('');
e.preventDefault();
}}
/>
);
} else {
appSection = (
<SettingItemMin
title='Applications (Preview)'
describe='Open to register a new third-party application'
updateSection={function updateSection() {
self.props.updateSection('app');
}}
/>
);
}
return (
<div>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>{'x'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
<i className='modal-back'></i>{'Developer Settings'}
</h4>
</div>
<div className='user-settings'>
<h3 className='tab-header'>{'Developer Settings'}</h3>
<div className='divider-dark first'/>
{appSection}
<div className='divider-dark'/>
</div>
</div>
);
}
}
DeveloperTab.defaultProps = {
activeSection: ''
};
DeveloperTab.propTypes = {
activeSection: React.PropTypes.string,
updateSection: React.PropTypes.func
};

Просмотреть файл

@@ -17,8 +17,8 @@ export default class UserSettingsModal extends React.Component {
$('body').on('click', '.modal-back', function changeDisplay() {
$(this).closest('.modal-dialog').removeClass('display--content');
});
$('body').on('click', '.modal-header .close', function closeModal() {
setTimeout(function finishClose() {
$('body').on('click', '.modal-header .close', () => {
setTimeout(() => {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
@@ -35,6 +35,9 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'});
tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'});
tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'});
if (global.window.config.EnableOAuthServiceProvider) {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
}
return (
<div
@@ -54,13 +57,13 @@ export default class UserSettingsModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
<span aria-hidden='true'>{'x'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
Account Settings
{'Account Settings'}
</h4>
</div>
<div className='modal-body'>

21
web/react/pages/authorize.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,21 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Authorize = require('../components/authorize.jsx');
function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) {
React.render(
<Authorize
teamName={teamName}
appName={appName}
responseType={responseType}
clientId={clientId}
redirectUri={redirectUri}
scope={scope}
state={state}
/>,
document.getElementById('authorize')
);
}
global.window.setup_authorize_page = setupAuthorizePage;

Просмотреть файл

@@ -33,6 +33,7 @@ var AccessHistoryModal = require('../components/access_history_modal.jsx');
var ActivityLogModal = require('../components/activity_log_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -222,6 +223,11 @@ function setupChannelPage(props) {
/>,
document.getElementById('file_upload_overlay')
);
React.render(
<RegisterAppModal />,
document.getElementById('register_app_modal')
);
}
global.window.setup_channel_page = setupChannelPage;

Просмотреть файл

@@ -987,3 +987,36 @@ export function updateValetFeature(data, success, error) {
track('api', 'api_teams_update_valet_feature');
}
export function registerOAuthApp(app, success, error) {
$.ajax({
url: '/api/v1/oauth/register',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(app),
success: success,
error: (xhr, status, err) => {
const e = handleError('registerApp', xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_apps_register');
}
export function allowOAuth2(responseType, clientId, redirectUri, state, scope, success, error) {
$.ajax({
url: '/api/v1/oauth/allow?response_type=' + responseType + '&client_id=' + clientId + '&redirect_uri=' + redirectUri + '&scope=' + scope + '&state=' + state,
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
error: (xhr, status, err) => {
const e = handleError('allowOAuth2', xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_allow_oauth2');
}

Просмотреть файл

@@ -315,3 +315,18 @@
}
}
.authorize-box {
margin: 100px auto;
width:500px;
height:280px;
border: 1px solid black;
}
.authorize-inner {
padding: 20px;
}
.authorize-btn {
margin-right: 6px;
}

26
web/templates/authorize.html Обычный файл
Просмотреть файл

@@ -0,0 +1,26 @@
{{define "authorize"}}
<html>
{{template "head" . }}
<body class="white">
<div class="container-fluid">
<div class="inner__wrap">
<div class="row content">
<div class="signup-header">
{{.Props.TeamName}}
</div>
<div class="col-sm-12">
<div id="authorize"></div>
</div>
<div class="footer-push"></div>
</div>
<div class="row footer">
{{template "footer" . }}
</div>
</div>
</div>
<script>
window.setup_authorize_page('{{ .Props.TeamName }}', '{{ .Props.AppName }}', '{{ .Props.ResponseType }}', '{{ .Props.ClientId }}', '{{ .Props.RedirectUri }}', '{{ .Props.Scope }}', '{{ .Props.State }}' );
</script>
</body>
</html>
{{end}}

Просмотреть файл

@@ -49,6 +49,7 @@
<div id="access_history_modal"></div>
<div id="activity_log_modal"></div>
<div id="removed_from_channel_modal"></div>
<div id="register_app_modal"></div>
<script>
window.setup_channel_page({{ .Props }});
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );

Просмотреть файл

@@ -4,19 +4,18 @@
package web
import (
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
l4g "code.google.com/p/log4go"
"fmt"
"github.com/gorilla/mux"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
"gopkg.in/fsnotify.v1"
"html/template"
"net/http"
"strconv"
"strings"
)
var Templates *template.Template
@@ -50,6 +49,8 @@ func InitWeb() {
mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET")
mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET")
mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST")
mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET")
mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET")
@@ -63,7 +64,7 @@ func InitWeb() {
mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET")
// ----------------------------------------------------------------------------------------------
// *ANYTHING* team spefic should go below this line
// *ANYTHING* team specific should go below this line
// ----------------------------------------------------------------------------------------------
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET")
@@ -648,3 +649,192 @@ func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("admin_console", "Admin Console")
page.Render(c, w)
}
func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewAppError("authorizeOAuth", "The system admin has turned off OAuth service providing.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if !CheckBrowserCompatability(c, r) {
return
}
responseType := r.URL.Query().Get("response_type")
clientId := r.URL.Query().Get("client_id")
redirect := r.URL.Query().Get("redirect_uri")
scope := r.URL.Query().Get("scope")
state := r.URL.Query().Get("state")
if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 {
c.Err = model.NewAppError("authorizeOAuth", "Missing one or more of response_type, client_id, or redirect_uri", "")
return
}
var app *model.OAuthApp
if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = result.Err
return
} else {
app = result.Data.(*model.OAuthApp)
}
var team *model.Team
if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
page := NewHtmlTemplatePage("authorize", "Authorize Application")
page.Props["TeamName"] = team.Name
page.Props["AppName"] = app.Name
page.Props["ResponseType"] = responseType
page.Props["ClientId"] = clientId
page.Props["RedirectUri"] = redirect
page.Props["Scope"] = scope
page.Props["State"] = state
page.Render(c, w)
}
func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewAppError("getAccessToken", "The system admin has turned off OAuth service providing.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
c.LogAudit("attempt")
r.ParseForm()
grantType := r.FormValue("grant_type")
if grantType != model.ACCESS_TOKEN_GRANT_TYPE {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad grant_type", "")
return
}
clientId := r.FormValue("client_id")
if len(clientId) != 26 {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad client_id", "")
return
}
secret := r.FormValue("client_secret")
if len(secret) == 0 {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing client_secret", "")
return
}
code := r.FormValue("code")
if len(code) == 0 {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing code", "")
return
}
redirectUri := r.FormValue("redirect_uri")
achan := api.Srv.Store.OAuth().GetApp(clientId)
tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code)
authData := api.GetAuthData(code)
if authData == nil {
c.LogAudit("fail - invalid auth code")
c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "")
return
}
uchan := api.Srv.Store.User().Get(authData.UserId)
if authData.IsExpired() {
c.LogAudit("fail - auth code expired")
c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "")
return
}
if authData.RedirectUri != redirectUri {
c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
c.Err = model.NewAppError("getAccessToken", "invalid_request: Supplied redirect_uri does not match authorization code redirect_uri", "")
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.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "")
return
}
var app *model.OAuthApp
if result := <-achan; result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "")
return
} else {
app = result.Data.(*model.OAuthApp)
}
if !model.ComparePassword(app.ClientSecret, secret) {
c.LogAudit("fail - invalid client credentials")
c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "")
return
}
callback := redirectUri
if len(callback) == 0 {
callback = app.CallbackUrls[0]
}
if result := <-tchan; result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while accessing database", "")
return
} else if result.Data != nil {
c.LogAudit("fail - auth code has been used previously")
accessData := result.Data.(*model.AccessData)
// Revoke access token, related auth code, and session from DB as well as from cache
if err := api.RevokeAccessToken(accessData.Token); err != nil {
l4g.Error("Encountered an error revoking an access token, err=" + err.Message)
}
c.Err = model.NewAppError("getAccessToken", "invalid_grant: Authorization code already exchanged for an access token", "")
return
}
var user *model.User
if result := <-uchan; result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while pulling user from database", "")
return
} else {
user = result.Data.(*model.User)
}
session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true}
if result := <-api.Srv.Store.Session().Save(session); result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving session to database", "")
return
} else {
session = result.Data.(*model.Session)
api.AddSessionToCache(session)
}
accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving access token to database", "")
return
}
accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: model.SESSION_TIME_OAUTH_IN_SECS}
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")
w.Write([]byte(accessRsp.ToJson()))
}

Просмотреть файл

@@ -6,8 +6,11 @@ package web
import (
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"net/http"
"net/url"
"strings"
"testing"
"time"
)
@@ -23,7 +26,7 @@ func Setup() {
api.InitApi()
InitWeb()
URL = "http://localhost:" + utils.Cfg.ServiceSettings.Port
ApiClient = model.NewClient(URL + "/api/v1")
ApiClient = model.NewClient(URL)
}
}
@@ -48,6 +51,135 @@ func TestStatic(t *testing.T) {
}
}
func TestGetAccessToken(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := ApiClient.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"}
ruser := ApiClient.Must(ApiClient.CreateUser(&user, "")).Data.(*model.User)
store.Must(api.Srv.Store.User().VerifyEmail(ruser.Id))
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{app.CallbackUrls[0]}}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - oauth providing turned off")
}
} else {
ApiClient.Must(ApiClient.LoginById(ruser.Id, "pwd"))
app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp)
redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"]
rurl, _ := url.Parse(redirect)
ApiClient.Logout()
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{app.Id}, "client_secret": []string{app.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{app.CallbackUrls[0]}}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad grant type")
}
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
data.Set("client_id", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing client id")
}
data.Set("client_id", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad client id")
}
data.Set("client_id", app.Id)
data.Set("client_secret", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing client secret")
}
data.Set("client_secret", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad client secret")
}
data.Set("client_secret", app.ClientSecret)
data.Set("code", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing code")
}
data.Set("code", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad code")
}
data.Set("code", rurl.Query().Get("code"))
data.Set("redirect_uri", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - non-matching redirect uri")
}
// reset data for successful request
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
data.Set("client_id", app.Id)
data.Set("client_secret", app.ClientSecret)
data.Set("code", rurl.Query().Get("code"))
data.Set("redirect_uri", app.CallbackUrls[0])
token := ""
if result, err := ApiClient.GetAccessToken(data); err != nil {
t.Fatal(err)
} else {
rsp := result.Data.(*model.AccessResponse)
if len(rsp.AccessToken) == 0 {
t.Fatal("access token not returned")
} else {
token = rsp.AccessToken
}
if rsp.TokenType != model.ACCESS_TOKEN_TYPE {
t.Fatal("access token type incorrect")
}
}
if result, err := ApiClient.DoApiGet("/users/profiles?access_token="+token, "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)
if len(userMap) == 0 {
t.Fatal("user map empty - did not get results correctly")
}
}
if _, err := ApiClient.DoApiGet("/users/profiles", "", ""); err == nil {
t.Fatal("should have failed - no access token provided")
}
if _, err := ApiClient.DoApiGet("/users/profiles?access_token=junk", "", ""); err == nil {
t.Fatal("should have failed - bad access token provided")
}
ApiClient.SetOAuthToken(token)
if result, err := ApiClient.DoApiGet("/users/profiles", "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)
if len(userMap) == 0 {
t.Fatal("user map empty - did not get results correctly")
}
}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - tried to reuse auth code")
}
ApiClient.ClearOAuthToken()
}
}
func TestZZWebTearDown(t *testing.T) {
// *IMPORTANT*
// This should be the last function in any test file