Merge pull request #693 from mattermost/mm-1419
Implement OAuth2 service provider functionality.
Этот коммит содержится в:
72
web/react/components/authorize.jsx
Обычный файл
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
Обычный файл
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/>;
|
||||
|
||||
93
web/react/components/user_settings_developer.jsx
Обычный файл
93
web/react/components/user_settings_developer.jsx
Обычный файл
@@ -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'>×</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
Обычный файл
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
Обычный файл
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]'} );
|
||||
|
||||
204
web/web.go
204
web/web.go
@@ -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()))
|
||||
}
|
||||
|
||||
134
web/web_test.go
134
web/web_test.go
@@ -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
|
||||
|
||||
Ссылка в новой задаче
Block a user