Merge pull request #8140 from edx/feature/shibboleth-tpa

Feature: Shibboleth/SAML SSO
This commit is contained in:
Braden MacDonald
2015-06-26 14:17:24 -07:00
89 changed files with 3016 additions and 733 deletions

View File

@@ -84,11 +84,13 @@
'js/student_account/views/FormView': 'js/student_account/views/FormView',
'js/student_account/models/LoginModel': 'js/student_account/models/LoginModel',
'js/student_account/views/LoginView': 'js/student_account/views/LoginView',
'js/student_account/views/InstitutionLoginView': 'js/student_account/views/InstitutionLoginView',
'js/student_account/models/PasswordResetModel': 'js/student_account/models/PasswordResetModel',
'js/student_account/views/PasswordResetView': 'js/student_account/views/PasswordResetView',
'js/student_account/models/RegisterModel': 'js/student_account/models/RegisterModel',
'js/student_account/views/RegisterView': 'js/student_account/views/RegisterView',
'js/student_account/views/AccessView': 'js/student_account/views/AccessView',
'js/student_account/views/HintedLoginView': 'js/student_account/views/HintedLoginView',
'js/student_profile/profile': 'js/student_profile/profile',
'js/student_profile/views/learner_profile_fields': 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory',
@@ -410,6 +412,14 @@
'js/student_account/views/FormView'
]
},
'js/student_account/views/InstitutionLoginView': {
exports: 'edx.student.account.InstitutionLoginView',
deps: [
'jquery',
'underscore',
'backbone'
]
},
'js/student_account/models/PasswordResetModel': {
exports: 'edx.student.account.PasswordResetModel',
deps: ['jquery', 'jquery.cookie', 'backbone']
@@ -439,6 +449,15 @@
'js/student_account/views/FormView'
]
},
'js/student_account/views/HintedLoginView': {
exports: 'edx.student.account.HintedLoginView',
deps: [
'jquery',
'underscore',
'backbone',
'gettext'
]
},
'js/student_account/views/AccessView': {
exports: 'edx.student.account.AccessView',
deps: [
@@ -450,6 +469,7 @@
'js/student_account/views/LoginView',
'js/student_account/views/PasswordResetView',
'js/student_account/views/RegisterView',
'js/student_account/views/InstitutionLoginView',
'js/student_account/models/LoginModel',
'js/student_account/models/PasswordResetModel',
'js/student_account/models/RegisterModel',
@@ -612,7 +632,9 @@
'lms/include/js/spec/student_account/account_spec.js',
'lms/include/js/spec/student_account/access_spec.js',
'lms/include/js/spec/student_account/finish_auth_spec.js',
'lms/include/js/spec/student_account/hinted_login_spec.js',
'lms/include/js/spec/student_account/login_spec.js',
'lms/include/js/spec/student_account/institution_login_spec.js',
'lms/include/js/spec/student_account/register_spec.js',
'lms/include/js/spec/student_account/password_reset_spec.js',
'lms/include/js/spec/student_account/enrollment_spec.js',

View File

@@ -58,6 +58,7 @@ define([
thirdPartyAuth: {
currentProvider: null,
providers: [],
secondaryProviders: [{name: "provider"}],
finishAuthUrl: finishAuthUrl
},
nextUrl: nextUrl, // undefined for default
@@ -97,6 +98,8 @@ define([
TemplateHelpers.installTemplate('templates/student_account/register');
TemplateHelpers.installTemplate('templates/student_account/password_reset');
TemplateHelpers.installTemplate('templates/student_account/form_field');
TemplateHelpers.installTemplate('templates/student_account/institution_login');
TemplateHelpers.installTemplate('templates/student_account/institution_register');
// Stub analytics tracking
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'pageview', 'trackLink']);
@@ -135,6 +138,30 @@ define([
assertForms('#login-form', '#register-form');
});
it('toggles between the login and institution login view', function() {
ajaxSpyAndInitialize(this, 'login');
// Simulate clicking on institution login button
$('#login-form .button-secondary-login[data-type="institution_login"]').click();
assertForms('#institution_login-form', '#login-form');
// Simulate selection of the login form
selectForm('login');
assertForms('#login-form', '#institution_login-form');
});
it('toggles between the register and institution register view', function() {
ajaxSpyAndInitialize(this, 'register');
// Simulate clicking on institution login button
$('#register-form .button-secondary-login[data-type="institution_login"]').click();
assertForms('#institution_login-form', '#register-form');
// Simulate selection of the login form
selectForm('register');
assertForms('#register-form', '#institution_login-form');
});
it('displays the reset password form', function() {
ajaxSpyAndInitialize(this, 'login');

View File

@@ -32,12 +32,14 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
var AUTH_DATA = {
'providers': [
{
'id': 'oa2-network1',
'name': "Network1",
'connected': true,
'connect_url': 'yetanother1.com/auth/connect',
'disconnect_url': 'yetanother1.com/auth/disconnect'
},
{
'id': 'oa2-network2',
'name': "Network2",
'connected': true,
'connect_url': 'yetanother2.com/auth/connect',

View File

@@ -0,0 +1,71 @@
define([
'jquery',
'underscore',
'common/js/spec_helpers/template_helpers',
'common/js/spec_helpers/ajax_helpers',
'js/student_account/views/HintedLoginView',
], function($, _, TemplateHelpers, AjaxHelpers, HintedLoginView) {
'use strict';
describe('edx.student.account.HintedLoginView', function() {
var view = null,
requests = null,
PLATFORM_NAME = 'edX',
THIRD_PARTY_AUTH = {
currentProvider: null,
providers: [
{
id: 'oa2-google-oauth2',
name: 'Google',
iconClass: 'fa-google-plus',
loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login',
registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register'
},
{
id: 'oa2-facebook',
name: 'Facebook',
iconClass: 'fa-facebook',
loginUrl: '/auth/login/facebook/?auth_entry=account_login',
registerUrl: '/auth/login/facebook/?auth_entry=account_register'
}
]
},
HINTED_PROVIDER = "oa2-google-oauth2";
var createHintedLoginView = function(test) {
// Initialize the login view
view = new HintedLoginView({
thirdPartyAuth: THIRD_PARTY_AUTH,
hintedProvider: HINTED_PROVIDER,
platformName: PLATFORM_NAME
});
// Mock the redirect call
spyOn( view, 'redirect' ).andCallFake( function() {} );
view.render();
};
beforeEach(function() {
setFixtures('<div id="hinted-login-form"></div>');
TemplateHelpers.installTemplate('templates/student_account/hinted_login');
});
it('displays a choice as two buttons', function() {
createHintedLoginView(this);
expect($('.proceed-button.button-oa2-google-oauth2')).toBeVisible();
expect($('.form-toggle')).toBeVisible();
expect($('.proceed-button.button-oa2-facebook')).not.toBeVisible();
});
it('redirects the user to the hinted provider if the user clicks the proceed button', function() {
createHintedLoginView(this);
// Click the "Yes, proceed" button
$('.proceed-button').click();
expect(view.redirect).toHaveBeenCalledWith( '/auth/login/google-oauth2/?auth_entry=account_login' );
});
});
});

View File

@@ -0,0 +1,80 @@
define([
'jquery',
'underscore',
'common/js/spec_helpers/template_helpers',
'js/student_account/views/InstitutionLoginView',
], function($, _, TemplateHelpers, InstitutionLoginView) {
'use strict';
describe('edx.student.account.InstitutionLoginView', function() {
var view = null,
PLATFORM_NAME = 'edX',
THIRD_PARTY_AUTH = {
currentProvider: null,
providers: [],
secondaryProviders: [
{
id: 'oa2-google-oauth2',
name: 'Google',
iconClass: 'fa-google-plus',
loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login',
registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register'
},
{
id: 'oa2-facebook',
name: 'Facebook',
iconClass: 'fa-facebook',
loginUrl: '/auth/login/facebook/?auth_entry=account_login',
registerUrl: '/auth/login/facebook/?auth_entry=account_register'
}
]
};
var createInstLoginView = function(mode) {
// Initialize the login view
view = new InstitutionLoginView({
mode: mode,
thirdPartyAuth: THIRD_PARTY_AUTH,
platformName: PLATFORM_NAME
});
view.render();
};
beforeEach(function() {
setFixtures('<div id="institution_login-form"></div>');
TemplateHelpers.installTemplate('templates/student_account/institution_login');
TemplateHelpers.installTemplate('templates/student_account/institution_register');
});
it('displays a list of providers', function() {
createInstLoginView('login');
expect($('#institution_login-form').html()).not.toBe("");
var $google = $('li a:contains("Google")');
expect($google).toBeVisible();
expect($google).toHaveAttr(
'href', '/auth/login/google-oauth2/?auth_entry=account_login'
);
var $facebook = $('li a:contains("Facebook")');
expect($facebook).toBeVisible();
expect($facebook).toHaveAttr(
'href', '/auth/login/facebook/?auth_entry=account_login'
);
});
it('displays a list of providers', function() {
createInstLoginView('register');
expect($('#institution_login-form').html()).not.toBe("");
var $google = $('li a:contains("Google")');
expect($google).toBeVisible();
expect($google).toHaveAttr(
'href', '/auth/login/google-oauth2/?auth_entry=account_register'
);
var $facebook = $('li a:contains("Facebook")');
expect($facebook).toBeVisible();
expect($facebook).toHaveAttr(
'href', '/auth/login/facebook/?auth_entry=account_register'
);
});
});
});

View File

@@ -25,12 +25,14 @@ define([
currentProvider: null,
providers: [
{
id: 'oa2-google-oauth2',
name: 'Google',
iconClass: 'fa-google-plus',
loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login',
registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register'
},
{
id: 'oa2-facebook',
name: 'Facebook',
iconClass: 'fa-facebook',
loginUrl: '/auth/login/facebook/?auth_entry=account_login',
@@ -195,8 +197,8 @@ define([
createLoginView(this);
// Verify that Google and Facebook registration buttons are displayed
expect($('.button-Google')).toBeVisible();
expect($('.button-Facebook')).toBeVisible();
expect($('.button-oa2-google-oauth2')).toBeVisible();
expect($('.button-oa2-facebook')).toBeVisible();
});
it('displays a link to the password reset form', function() {

View File

@@ -32,12 +32,14 @@ define([
currentProvider: null,
providers: [
{
id: 'oa2-google-oauth2',
name: 'Google',
iconClass: 'fa-google-plus',
loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login',
registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register'
},
{
id: 'oa2-facebook',
name: 'Facebook',
iconClass: 'fa-facebook',
loginUrl: '/auth/login/facebook/?auth_entry=account_login',
@@ -284,8 +286,8 @@ define([
createRegisterView(this);
// Verify that Google and Facebook registration buttons are displayed
expect($('.button-Google')).toBeVisible();
expect($('.button-Facebook')).toBeVisible();
expect($('.button-oa2-google-oauth2')).toBeVisible();
expect($('.button-oa2-facebook')).toBeVisible();
});
it('validates registration form fields', function() {

View File

@@ -11,6 +11,7 @@ var edx = edx || {};
return new edx.student.account.AccessView({
mode: container.data('initial-mode'),
thirdPartyAuth: container.data('third-party-auth'),
thirdPartyAuthHint: container.data('third-party-auth-hint'),
nextUrl: container.data('next-url'),
platformName: container.data('platform-name'),
loginFormDesc: container.data('login-form-desc'),

View File

@@ -18,7 +18,9 @@ var edx = edx || {};
subview: {
login: {},
register: {},
passwordHelp: {}
passwordHelp: {},
institutionLogin: {},
hintedLogin: {}
},
nextUrl: '/dashboard',
@@ -42,6 +44,8 @@ var edx = edx || {};
providers: []
};
this.thirdPartyAuthHint = obj.thirdPartyAuthHint || null;
if (obj.nextUrl) {
// Ensure that the next URL is internal for security reasons
if ( ! window.isExternal( obj.nextUrl ) ) {
@@ -52,7 +56,9 @@ var edx = edx || {};
this.formDescriptions = {
login: obj.loginFormDesc,
register: obj.registrationFormDesc,
reset: obj.passwordResetFormDesc
reset: obj.passwordResetFormDesc,
institution_login: null,
hinted_login: null
};
this.platformName = obj.platformName;
@@ -148,6 +154,26 @@ var edx = edx || {};
// Listen for 'auth-complete' event so we can enroll/redirect the user appropriately.
this.listenTo( this.subview.register, 'auth-complete', this.authComplete );
},
institution_login: function ( unused ) {
this.subview.institutionLogin = new edx.student.account.InstitutionLoginView({
thirdPartyAuth: this.thirdPartyAuth,
platformName: this.platformName,
mode: this.activeForm
});
this.subview.institutionLogin.render();
},
hinted_login: function ( unused ) {
this.subview.hintedLogin = new edx.student.account.HintedLoginView({
thirdPartyAuth: this.thirdPartyAuth,
hintedProvider: this.thirdPartyAuthHint,
platformName: this.platformName
});
this.subview.hintedLogin.render();
}
},
@@ -180,9 +206,11 @@ var edx = edx || {};
category: 'user-engagement'
});
if ( !this.form.isLoaded( $form ) ) {
// Load the form. Institution login is always refreshed since it changes based on the previous form.
if ( !this.form.isLoaded( $form ) || type == "institution_login") {
this.loadForm( type );
}
this.activeForm = type;
this.element.hide( $(this.el).find('.submission-success') );
this.element.hide( $(this.el).find('.form-wrapper') );
@@ -190,11 +218,13 @@ var edx = edx || {};
this.element.scrollTop( $anchor );
// Update url without reloading page
History.pushState( null, document.title, '/' + type + queryStr );
if (type != "institution_login") {
History.pushState( null, document.title, '/' + type + queryStr );
}
analytics.page( 'login_and_registration', type );
// Focus on the form
document.getElementById(type).focus();
$("#" + type).focus();
},
/**

View File

@@ -215,7 +215,9 @@ var edx = edx || {};
submitForm: function( event ) {
var data = this.getFormData();
event.preventDefault();
if (!_.isUndefined(event)) {
event.preventDefault();
}
this.toggleDisableButton(true);

View File

@@ -0,0 +1,52 @@
var edx = edx || {};
(function($, _, gettext) {
'use strict';
edx.student = edx.student || {};
edx.student.account = edx.student.account || {};
edx.student.account.HintedLoginView = Backbone.View.extend({
el: '#hinted-login-form',
tpl: '#hinted_login-tpl',
events: {
'click .proceed-button': 'proceedWithHintedAuth'
},
formType: 'hinted-login',
initialize: function( data ) {
this.tpl = $(this.tpl).html();
this.providers = data.thirdPartyAuth.providers || [];
this.hintedProvider = _.findWhere(this.providers, {id: data.hintedProvider})
this.platformName = data.platformName;
},
render: function() {
$(this.el).html( _.template( this.tpl, {
// We pass the context object to the template so that
// we can perform variable interpolation using sprintf
providers: this.providers,
platformName: this.platformName,
hintedProvider: this.hintedProvider
}));
return this;
},
proceedWithHintedAuth: function( event ) {
this.redirect(this.hintedProvider.loginUrl);
},
/**
* Redirect to a URL. Mainly useful for mocking out in tests.
* @param {string} url The URL to redirect to.
*/
redirect: function( url ) {
window.location.href = url;
}
});
})(jQuery, _, gettext);

View File

@@ -0,0 +1,30 @@
var edx = edx || {};
(function($, _, Backbone) {
'use strict';
edx.student = edx.student || {};
edx.student.account = edx.student.account || {};
edx.student.account.InstitutionLoginView = Backbone.View.extend({
el: '#institution_login-form',
initialize: function( data ) {
var tpl = data.mode == "register" ? '#institution_register-tpl' : '#institution_login-tpl';
this.tpl = $(tpl).html();
this.providers = data.thirdPartyAuth.secondaryProviders || [];
this.platformName = data.platformName;
},
render: function() {
$(this.el).html( _.template( this.tpl, {
// We pass the context object to the template so that
// we can perform variable interpolation using sprintf
providers: this.providers,
platformName: this.platformName
}));
return this;
}
});
})(jQuery, _, Backbone);

View File

@@ -25,6 +25,9 @@ var edx = edx || {};
preRender: function( data ) {
this.providers = data.thirdPartyAuth.providers || [];
this.hasSecondaryProviders = (
data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length
);
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
this.errorMessage = data.thirdPartyAuth.errorMessage || '';
this.platformName = data.platformName;
@@ -45,6 +48,7 @@ var edx = edx || {};
currentProvider: this.currentProvider,
errorMessage: this.errorMessage,
providers: this.providers,
hasSecondaryProviders: this.hasSecondaryProviders,
platformName: this.platformName
}
}));

View File

@@ -22,9 +22,13 @@ var edx = edx || {};
preRender: function( data ) {
this.providers = data.thirdPartyAuth.providers || [];
this.hasSecondaryProviders = (
data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length
);
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
this.errorMessage = data.thirdPartyAuth.errorMessage || '';
this.platformName = data.platformName;
this.autoSubmit = data.thirdPartyAuth.autoSubmitRegForm;
this.listenTo( this.model, 'sync', this.saveSuccess );
},
@@ -41,12 +45,19 @@ var edx = edx || {};
currentProvider: this.currentProvider,
errorMessage: this.errorMessage,
providers: this.providers,
hasSecondaryProviders: this.hasSecondaryProviders,
platformName: this.platformName
}
}));
this.postRender();
if (this.autoSubmit) {
$(this.el).hide();
$('#register-honor_code').prop('checked', true);
this.submitForm();
}
return this;
},
@@ -63,6 +74,7 @@ var edx = edx || {};
},
saveError: function( error ) {
$(this.el).show(); // Show in case the form was hidden for auto-submission
this.errors = _.flatten(
_.map(
JSON.parse(error.responseText),
@@ -76,6 +88,13 @@ var edx = edx || {};
);
this.setErrors();
this.toggleDisableButton(false);
}
},
postFormSubmission: function() {
if (_.compact(this.errors).length) {
// The form did not get submitted due to validation errors.
$(this.el).show(); // Show in case the form was hidden for auto-submission
}
},
});
})(jQuery, _, gettext);

View File

@@ -137,7 +137,7 @@
screenReaderTitle: interpolate_text(
gettext("Connect your {accountName} account"), {accountName: provider['name']}
),
valueAttribute: 'auth-' + provider.name.toLowerCase(),
valueAttribute: 'auth-' + provider.id,
helpMessage: '',
connected: provider.connected,
connectUrl: provider.connect_url,

View File

@@ -532,30 +532,30 @@
margin-right: 0;
}
&.button-Google:hover, &.button-Google:focus {
&.button-oa2-google-oauth2:hover, &.button-oa2-google-oauth2:focus {
background-color: #dd4b39;
border: 1px solid #A5382B;
}
&.button-Google:hover {
&.button-oa2-google-oauth2:hover {
box-shadow: 0 2px 1px 0 #8D3024;
}
&.button-Facebook:hover, &.button-Facebook:focus {
&.button-oa2-facebook:hover, &.button-oa2-facebook:focus {
background-color: #3b5998;
border: 1px solid #263A62;
}
&.button-Facebook:hover {
&.button-oa2-facebook:hover {
box-shadow: 0 2px 1px 0 #30487C;
}
&.button-LinkedIn:hover , &.button-LinkedIn:focus {
&.button-oa2-linkedin-oauth2:hover , &.button-oa2-linkedin-oauth2:focus {
background-color: #0077b5;
border: 1px solid #06527D;
}
&.button-LinkedIn:hover {
&.button-oa2-linkedin-oauth2:hover {
box-shadow: 0 2px 1px 0 #005D8E;
}

View File

@@ -14,6 +14,7 @@ $sm-btn-linkedin: #0077b5;
background: $white;
min-height: 100%;
width: 100%;
$third-party-button-height: ($baseline*1.75);
h2 {
@extend %t-title5;
@@ -22,6 +23,10 @@ $sm-btn-linkedin: #0077b5;
font-family: $sans-serif;
}
.instructions {
@extend %t-copy-base;
}
/* Temp. fix until applied globally */
> {
@include box-sizing(border-box);
@@ -67,10 +72,11 @@ $sm-btn-linkedin: #0077b5;
}
}
form {
form,
.wrapper-other-login {
border: 1px solid $gray-l4;
border-radius: 5px;
padding: 0px 25px 20px 25px;
border-radius: ($baseline/4);
padding: 0 ($baseline*1.25) $baseline ($baseline*1.25);
}
.section-title {
@@ -106,16 +112,20 @@ $sm-btn-linkedin: #0077b5;
}
}
.nav-btn {
%nav-btn-base {
@extend %btn-secondary-blue-outline;
width: 100%;
height: ($baseline*2);
text-transform: none;
text-shadow: none;
font-weight: 600;
letter-spacing: normal;
}
.nav-btn {
@extend %nav-btn-base;
@extend %t-strong;
}
.form-type,
.toggle-form {
@include box-sizing(border-box);
@@ -348,29 +358,31 @@ $sm-btn-linkedin: #0077b5;
.login-provider {
@extend %btn-secondary-grey-outline;
width: 130px;
padding: 0 0 0 ($baseline*2);
height: 34px;
text-align: left;
@extend %t-action4;
@include padding(0, 0, 0, $baseline*2);
@include text-align(left);
position: relative;
margin-right: ($baseline/4);
margin-bottom: $baseline;
border-color: $lightGrey1;
width: $baseline*6.5;
height: $third-party-button-height;
text-shadow: none;
text-transform: none;
position: relative;
font-size: 0.8em;
border-color: $lightGrey1;
&:nth-of-type(odd) {
margin-right: 13px;
}
.icon {
color: white;
@include left(0);
position: absolute;
top: -1px;
left: 0;
width: 30px;
height: 34px;
line-height: 34px;
bottom: -1px;
background: $m-blue-d3;
line-height: $third-party-button-height;
text-align: center;
color: $white;
}
&:hover,
@@ -378,17 +390,13 @@ $sm-btn-linkedin: #0077b5;
background-image: none;
.icon {
height: 32px;
line-height: 32px;
top: 0;
bottom: 0;
line-height: ($third-party-button-height - 2px);
}
}
&:last-child {
margin-bottom: $baseline;
}
&.button-Google {
&.button-oa2-google-oauth2 {
color: $sm-btn-google;
.icon {
@@ -407,7 +415,7 @@ $sm-btn-linkedin: #0077b5;
}
}
&.button-Facebook {
&.button-oa2-facebook {
color: $sm-btn-facebook;
.icon {
@@ -426,7 +434,7 @@ $sm-btn-linkedin: #0077b5;
}
}
&.button-LinkedIn {
&.button-oa2-linkedin-oauth2 {
color: $sm-btn-linkedin;
.icon {
@@ -447,6 +455,19 @@ $sm-btn-linkedin: #0077b5;
}
.button-secondary-login {
@extend %nav-btn-base;
@extend %t-action4;
@extend %t-regular;
border-color: $lightGrey1;
padding: 0;
height: $third-party-button-height;
&:hover {
border-color: $m-blue-d3;
}
}
/** Error Container - from _account.scss **/
.status {
@include box-sizing(border-box);
@@ -503,6 +524,13 @@ $sm-btn-linkedin: #0077b5;
}
}
.institution-list {
.institution {
@extend %t-copy-base;
}
}
@include media( max-width 330px) {
.form-type {
width: 98%;