/*******************************************************************************
* Copyright 2018 The MIT Internet Trust Consortium
*
* Portions copyright 2011-2013 The MITRE Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
var DynRegClient = Backbone.Model.extend({
idAttribute: "client_id",
defaults: {
client_id: null,
client_secret: null,
redirect_uris: [],
client_name: null,
client_uri: null,
logo_uri: null,
contacts: [],
tos_uri: null,
token_endpoint_auth_method: null,
scope: null,
grant_types: [],
response_types: [],
policy_uri: null,
jwks_uri: null,
jwks: null,
jwksType: 'URI',
application_type: null,
sector_identifier_uri: null,
subject_type: null,
request_object_signing_alg: null,
userinfo_signed_response_alg: null,
userinfo_encrypted_response_alg: null,
userinfo_encrypted_response_enc: null,
id_token_signed_response_alg: null,
id_token_encrypted_response_alg: null,
id_token_encrypted_response_enc: null,
default_max_age: null,
require_auth_time: false,
default_acr_values: null,
initiate_login_uri: null,
post_logout_redirect_uris: null,
claims_redirect_uris: [],
request_uris: [],
software_statement: null,
software_id: null,
software_version: null,
code_challenge_method: null,
registration_access_token: null,
registration_client_uri: null
},
sync: function(method, model, options) {
if (model.get('registration_access_token')) {
var headers = options.headers ? options.headers : {};
headers['Authorization'] = 'Bearer ' + model.get('registration_access_token');
options.headers = headers;
}
return this.constructor.__super__.sync(method, model, options);
},
urlRoot: 'register'
});
var DynRegRootView = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.options = options;
},
events: {
"click #newreg": "newReg",
"click #editreg": "editReg"
},
load: function(callback) {
if (this.options.systemScopeList.isFetched) {
callback();
return;
}
$('#loadingbox').sheet('show');
$('#loading').html('' + $.t('common.scopes') + ' ');
$.when(this.options.systemScopeList.fetchIfNeeded({
success: function(e) {
$('#loading-scopes').addClass('label-success');
},
error: app.errorHandlerView.handleError()
})).done(function() {
$('#loadingbox').sheet('hide');
callback();
});
},
render: function() {
$(this.el).html($('#tmpl-dynreg').html());
$(this.el).i18n();
return this;
},
newReg: function(e) {
e.preventDefault();
this.remove();
app.navigate('dev/dynreg/new', {
trigger: true
});
},
editReg: function(e) {
e.preventDefault();
var clientId = $('#clientId').val();
var token = $('#regtoken').val();
var client = new DynRegClient({
client_id: clientId,
registration_access_token: token
});
var self = this;
client.fetch({
success: function() {
var userInfo = getUserInfo();
var contacts = client.get("contacts");
if (userInfo != null && userInfo.email != null && !_.contains(contacts, userInfo.email)) {
contacts.push(userInfo.email);
}
client.set({
contacts: contacts
}, {
silent: true
});
if (client.get("jwks")) {
client.set({
jwksType: "VAL"
}, {
silent: true
});
} else {
client.set({
jwksType: "URI"
}, {
silent: true
});
}
var view = new DynRegEditView({
model: client,
systemScopeList: app.systemScopeList
});
view.load(function() {
$('#content').html(view.render().el);
view.delegateEvents();
setPageTitle($.t('dynreg.edit-dynamically-registered'));
app.navigate('dev/dynreg/edit', {
trigger: true
});
self.remove();
});
},
error: app.errorHandlerView.handleError({
message: $.t('dynreg.invalid-access-token')
})
});
}
});
var DynRegEditView = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.options = options;
if (!this.template) {
this.template = _.template($('#tmpl-dynreg-client-form').html());
}
this.redirectUrisCollection = new Backbone.Collection();
this.scopeCollection = new Backbone.Collection();
this.contactsCollection = new Backbone.Collection();
this.defaultAcrValuesCollection = new Backbone.Collection();
this.requestUrisCollection = new Backbone.Collection();
this.postLogoutRedirectUrisCollection = new Backbone.Collection();
this.claimsRedirectUrisCollection = new Backbone.Collection();
this.listWidgetViews = [];
},
load: function(callback) {
if (this.options.systemScopeList.isFetched) {
callback();
return;
}
$('#loadingbox').sheet('show');
$('#loading').html('' + $.t('common.scopes') + ' ');
$.when(this.options.systemScopeList.fetchIfNeeded({
success: function(e) {
$('#loading-scopes').addClass('label-success');
},
error: app.errorHandlerView.handleError()
})).done(function() {
$('#loadingbox').sheet('hide');
callback();
});
},
events: {
"click .btn-save": "saveClient",
"click .btn-cancel": "cancel",
"click .btn-delete": "deleteClient",
"change #logoUri input": "previewLogo",
"change #tokenEndpointAuthMethod input:radio": "toggleClientCredentials",
"change #jwkSelector input:radio": "toggleJWKSetType"
},
cancel: function(e) {
e.preventDefault();
app.navigate('dev/dynreg', {
trigger: true
});
},
deleteClient: function(e) {
e.preventDefault();
if (confirm($.t('client.client-table.confirm'))) {
var self = this;
this.model.destroy({
dataType: false,
processData: false,
success: function() {
self.remove();
app.navigate('dev/dynreg', {
trigger: true
});
},
error: app.errorHandlerView.handleError({
"log": "An error occurred when deleting a client"
})
});
}
return false;
},
previewLogo: function() {
if ($('#logoUri input', this.el).val()) {
$('#logoPreview', this.el).empty();
$('#logoPreview', this.el).attr('src', $('#logoUri input', this.el).val());
} else {
// $('#logoBlock', this.el).hide();
$('#logoPreview', this.el).attr('src', 'resources/images/logo_placeholder.gif');
}
},
/**
* Set up the form based on the current state of the tokenEndpointAuthMethod
* parameter
*
* @param event
*/
toggleClientCredentials: function() {
var tokenEndpointAuthMethod = $('#tokenEndpointAuthMethod input', this.el).filter(':checked').val();
// show or hide the signing algorithm method depending on what's
// selected
if (tokenEndpointAuthMethod == 'private_key_jwt' || tokenEndpointAuthMethod == 'client_secret_jwt') {
$('#tokenEndpointAuthSigningAlg', this.el).show();
} else {
$('#tokenEndpointAuthSigningAlg', this.el).hide();
}
},
/**
* Set up the form based on the JWK Set selector
*/
toggleJWKSetType: function() {
var jwkSelector = $('#jwkSelector input:radio', this.el).filter(':checked').val();
if (jwkSelector == 'URI') {
$('#jwksUri', this.el).show();
$('#jwks', this.el).hide();
} else if (jwkSelector == 'VAL') {
$('#jwksUri', this.el).hide();
$('#jwks', this.el).show();
} else {
$('#jwksUri', this.el).hide();
$('#jwks', this.el).hide();
}
},
disableUnsupportedJOSEItems: function(serverSupported, query) {
var supported = ['default'];
if (serverSupported) {
supported = _.union(supported, serverSupported);
}
$(query, this.$el).each(function(idx) {
if (_.contains(supported, $(this).val())) {
$(this).prop('disabled', false);
} else {
$(this).prop('disabled', true);
}
});
},
// returns "null" if given the value "default" as a string,
// otherwise returns input value. useful for parsing the JOSE
// algorithm dropdowns
defaultToNull: function(value) {
if (value == 'default') {
return null;
} else {
return value;
}
},
// returns "null" if the given value is falsy
emptyToNull: function(value) {
if (value) {
return value;
} else {
return null;
}
},
// maps from a form-friendly name to the real grant parameter name
grantMap: {
'authorization_code': 'authorization_code',
'password': 'password',
'implicit': 'implicit',
'client_credentials': 'client_credentials',
'redelegate': 'urn:ietf:params:oauth:grant_type:redelegate',
'refresh_token': 'refresh_token'
},
// maps from a form-friendly name to the real response type
// parameter name
responseMap: {
'code': 'code',
'token': 'token',
'idtoken': 'id_token',
'token-idtoken': 'token id_token',
'code-idtoken': 'code id_token',
'code-token': 'code token',
'code-token-idtoken': 'code token id_token'
},
saveClient: function(e) {
e.preventDefault();
$('.control-group').removeClass('error');
// sync any leftover collection items
_.each(this.listWidgetViews, function(v) {
v.addItem($.Event('click'));
});
// build the scope object
var scopes = this.scopeCollection.pluck("item").join(" ");
// build the grant type object
var grantTypes = [];
$.each(this.grantMap, function(index, type) {
if ($('#grantTypes-' + index).is(':checked')) {
grantTypes.push(type);
}
});
// build the response type object
var responseTypes = [];
$.each(this.responseMap, function(index, type) {
if ($('#responseTypes-' + index).is(':checked')) {
responseTypes.push(type);
}
});
var contacts = this.contactsCollection.pluck('item');
var userInfo = getUserInfo();
if (userInfo && userInfo.email) {
if (!_.contains(contacts, userInfo.email)) {
contacts.push(userInfo.email);
}
}
// make sure that the subject identifier is consistent with the
// redirect URIs
var subjectType = $('#subjectType input').filter(':checked').val();
var redirectUris = this.redirectUrisCollection.pluck("item");
var sectorIdentifierUri = $('#sectorIdentifierUri input').val();
if (subjectType == 'PAIRWISE' && redirectUris.length > 1 && sectorIdentifierUri == '') {
// Display an alert with an error message
app.errorHandlerView.showErrorMessage($.t("client.client-form.error.consistency"), $.t("client.client-form.error.pairwise-sector"));
return false;
}
// process the JWKS
var jwksUri = null;
var jwks = null;
var jwkSelector = $('#jwkSelector input:radio', this.el).filter(':checked').val();
if (jwkSelector == 'URI') {
jwksUri = $('#jwksUri input').val();
jwks = null;
} else if (jwkSelector == 'VAL') {
jwksUri = null;
try {
jwks = JSON.parse($('#jwks textarea').val());
} catch (e) {
console.log("An error occurred when parsing the JWK Set");
app.errorHandlerView.showErrorMessage($.t("client.client-form.error.jwk-set"), $.t("client.client-form.error.jwk-set-parse"));
return false;
}
} else {
jwksUri = null;
jwks = null;
}
var attrs = {
client_name: this.emptyToNull($('#clientName input').val()),
redirect_uris: redirectUris,
logo_uri: this.emptyToNull($('#logoUri input').val()),
grant_types: grantTypes,
scope: scopes,
client_secret: null, // never send a client secret
tos_uri: this.emptyToNull($('#tosUri input').val()),
policy_uri: this.emptyToNull($('#policyUri input').val()),
client_uri: this.emptyToNull($('#clientUri input').val()),
application_type: $('#applicationType input').filter(':checked').val(),
jwks_uri: jwksUri,
jwks: jwks,
subject_type: subjectType,
software_statement: this.emptyToNull($('#softwareStatement textarea').val()),
softwareId: this.emptyToNull($('#softwareId input').val()),
softwareVersion: this.emptyToNull($('#softwareVersion input').val()),
token_endpoint_auth_method: $('#tokenEndpointAuthMethod input').filter(':checked').val(),
response_types: responseTypes,
sector_identifier_uri: sectorIdentifierUri,
initiate_login_uri: this.emptyToNull($('#initiateLoginUri input').val()),
post_logout_redirect_uris: this.postLogoutRedirectUrisCollection.pluck('item'),
claims_redirect_uris: this.claimsRedirectUrisCollection.pluck('item'),
require_auth_time: $('#requireAuthTime input').is(':checked'),
default_max_age: parseInt($('#defaultMaxAge input').val()),
contacts: contacts,
request_uris: this.requestUrisCollection.pluck('item'),
default_acr_values: this.defaultAcrValuesCollection.pluck('item'),
request_object_signing_alg: this.defaultToNull($('#requestObjectSigningAlg select').val()),
userinfo_signed_response_alg: this.defaultToNull($('#userInfoSignedResponseAlg select').val()),
userinfo_encrypted_response_alg: this.defaultToNull($('#userInfoEncryptedResponseAlg select').val()),
userinfo_encrypted_response_enc: this.defaultToNull($('#userInfoEncryptedResponseEnc select').val()),
id_token_signed_response_alg: this.defaultToNull($('#idTokenSignedResponseAlg select').val()),
id_token_encrypted_response_alg: this.defaultToNull($('#idTokenEncryptedResponseAlg select').val()),
id_token_encrypted_response_enc: this.defaultToNull($('#idTokenEncryptedResponseEnc select').val()),
token_endpoint_auth_signing_alg: this.defaultToNull($('#tokenEndpointAuthSigningAlg select').val()),
code_challenge_method: this.defaultToNull($('#codeChallengeMethod select').val())
};
// set all empty strings to nulls
for ( var key in attrs) {
if (attrs[key] === "") {
attrs[key] = null;
}
}
var _self = this;
this.model.save(attrs, {
success: function() {
// switch to an "edit" view
app.navigate('dev/dynreg/edit', {
trigger: true
});
_self.remove();
var userInfo = getUserInfo();
var contacts = _self.model.get("contacts");
if (userInfo != null && userInfo.email != null && !_.contains(contacts, userInfo.email)) {
contacts.push(userInfo.email);
}
_self.model.set({
contacts: contacts
}, {
silent: true
});
if (_self.model.get("jwks")) {
_self.model.set({
jwksType: "VAL"
}, {
silent: true
});
} else {
_self.model.set({
jwksType: "URI"
}, {
silent: true
});
}
var view = new DynRegEditView({
model: _self.model,
systemScopeList: _self.options.systemScopeList
});
view.load(function() {
// reload
$('#content').html(view.render().el);
view.delegateEvents();
});
},
error: app.errorHandlerView.handleError({
log: "An error occurred when saving a client"
})
});
return false;
},
render: function() {
var data = {
client: this.model.toJSON(),
userInfo: getUserInfo(),
heartMode: heartMode
};
$(this.el).html(this.template(data));
this.listWidgetViews = [];
var _self = this;
// build and bind registered redirect URI collection and view
_.each(this.model.get("redirect_uris"), function(redirectUri) {
_self.redirectUrisCollection.add(new URIModel({
item: redirectUri
}));
});
var redirectUriView = new ListWidgetView({
type: 'uri',
placeholder: 'https://',
helpBlockText: $.t('client.client-form.redirect-uris-help'),
collection: this.redirectUrisCollection
});
$("#redirectUris .controls", this.el).html(redirectUriView.render().el);
this.listWidgetViews.push(redirectUriView);
// build and bind scopes
var scopes = this.model.get("scope");
var scopeSet = scopes ? scopes.split(" ") : [];
_.each(scopeSet, function(scope) {
_self.scopeCollection.add(new Backbone.Model({
item: scope
}));
});
var scopeView = new ListWidgetView({
placeholder: $.t('client.client-form.scope-placeholder'),
autocomplete: _.uniq(_.flatten(this.options.systemScopeList.unrestrictedScopes().pluck("value"))),
helpBlockText: $.t('client.client-form.scope-help'),
collection: this.scopeCollection
});
$("#scope .controls", this.el).html(scopeView.render().el);
this.listWidgetViews.push(scopeView);
// build and bind contacts
_.each(this.model.get('contacts'), function(contact) {
_self.contactsCollection.add(new Backbone.Model({
item: contact
}));
});
var contactView = new ListWidgetView({
placeholder: $.t('client.client-form.contacts-placeholder'),
helpBlockText: $.t('client.client-form.contacts-help'),
collection: this.contactsCollection
});
$("#contacts .controls div", this.el).html(contactView.render().el);
this.listWidgetViews.push(contactView);
// build and bind post-logout redirect URIs
_.each(this.model.get('post_logout_redirect_uris'), function(postLogoutRedirectUri) {
_self.postLogoutRedirectUrisCollection.add(new URIModel({
item: postLogoutRedirectUri
}));
});
var postLogoutRedirectUrisView = new ListWidgetView({
type: 'uri',
placeholder: 'https://',
helpBlockText: $.t('client.client-form.post-logout-help'),
collection: this.postLogoutRedirectUrisCollection
});
$('#postLogoutRedirectUris .controls', this.el).html(postLogoutRedirectUrisView.render().el);
this.listWidgetViews.push(postLogoutRedirectUrisView);
// build and bind claims redirect URIs
_.each(this.model.get('claimsRedirectUris'), function(claimsRedirectUri) {
_self.claimsRedirectUrisCollection.add(new URIModel({
item: claimsRedirectUri
}));
});
var claimsRedirectUrisView = new ListWidgetView({
type: 'uri',
placeholder: 'https://',
helpBlockText: $.t('client.client-form.claims-redirect-uris-help'),
collection: this.claimsRedirectUrisCollection
});
$('#claimsRedirectUris .controls', this.el).html(claimsRedirectUrisView.render().el);
this.listWidgetViews.push(claimsRedirectUrisView);
// build and bind request URIs
_.each(this.model.get('request_uris'), function(requestUri) {
_self.requestUrisCollection.add(new URIModel({
item: requestUri
}));
});
var requestUriView = new ListWidgetView({
type: 'uri',
placeholder: 'https://',
helpBlockText: $.t('client.client-form.request-uri-help'),
collection: this.requestUrisCollection
});
$('#requestUris .controls', this.el).html(requestUriView.render().el);
this.listWidgetViews.push(requestUriView);
// build and bind default ACR values
_.each(this.model.get('default_acr_values'), function(defaultAcrValue) {
_self.defaultAcrValuesCollection.add(new Backbone.Model({
item: defaultAcrValue
}));
});
var defaultAcrView = new ListWidgetView({
placeholder: $.t('client.client-form.acr-values-placeholder'),
// TODO: autocomplete from spec
helpBlockText: $.t('client.client-form.acr-values-help'),
collection: this.defaultAcrValuesCollection
});
$('#defaultAcrValues .controls', this.el).html(defaultAcrView.render().el);
this.listWidgetViews.push(defaultAcrView);
this.toggleClientCredentials();
this.previewLogo();
this.toggleJWKSetType();
// disable unsupported JOSE algorithms
this.disableUnsupportedJOSEItems(app.serverConfiguration.request_object_signing_alg_values_supported, '#requestObjectSigningAlg option');
this.disableUnsupportedJOSEItems(app.serverConfiguration.userinfo_signing_alg_values_supported, '#userInfoSignedResponseAlg option');
this.disableUnsupportedJOSEItems(app.serverConfiguration.userinfo_encryption_alg_values_supported, '#userInfoEncryptedResponseAlg option');
this.disableUnsupportedJOSEItems(app.serverConfiguration.userinfo_encryption_enc_values_supported, '#userInfoEncryptedResponseEnc option');
this.disableUnsupportedJOSEItems(app.serverConfiguration.id_token_signing_alg_values_supported, '#idTokenSignedResponseAlg option');
this.disableUnsupportedJOSEItems(app.serverConfiguration.id_token_encryption_alg_values_supported, '#idTokenEncryptedResponseAlg option');
this.disableUnsupportedJOSEItems(app.serverConfiguration.id_token_encryption_enc_values_supported, '#idTokenEncryptedResponseEnc option');
this.disableUnsupportedJOSEItems(app.serverConfiguration.token_endpoint_auth_signing_alg_values_supported, '#tokenEndpointAuthSigningAlg option');
this.$('.nyi').clickover({
placement: 'right',
title: $.t('common.not-yet-implemented'),
content: $.t('common.not-yet-implemented-content')
});
$(this.el).i18n();
return this;
}
});
ui.routes.push({
path: "dev/dynreg",
name: "dynReg",
callback: function() {
this.breadCrumbView.collection.reset();
this.breadCrumbView.collection.add([{
text: $.t('admin.home'),
href: ""
}, {
text: $.t('admin.self-service-client'),
href: "manage/#dev/dynreg"
}]);
var view = new DynRegRootView({
systemScopeList: this.systemScopeList
});
this.updateSidebar('dev/dynreg');
view.load(function() {
$('#content').html(view.render().el);
setPageTitle($.t('admin.self-service-client'));
});
}
});
ui.routes.push({
path: "dev/dynreg/new",
name: "newDynReg",
callback: function() {
this.breadCrumbView.collection.reset();
this.breadCrumbView.collection.add([{
text: $.t('admin.home'),
href: ""
}, {
text: $.t('admin.self-service-client'),
href: "manage/#dev/dynreg"
}, {
text: $.t('dynreg.new-client'),
href: "manage/#dev/dynreg/new"
}]);
this.updateSidebar('dev/dynreg');
var client = new DynRegClient();
var view = new DynRegEditView({
model: client,
systemScopeList: this.systemScopeList
});
view.load(function() {
var userInfo = getUserInfo();
var contacts = [];
if (userInfo != null && userInfo.email != null) {
contacts.push(userInfo.email);
}
if (heartMode) {
client.set({
require_auth_time: true,
default_max_age: 60000,
scope: _.uniq(_.flatten(app.systemScopeList.defaultUnrestrictedScopes().pluck("value"))).join(" "),
token_endpoint_auth_method: 'private_key_jwt',
grant_types: ["authorization_code"],
response_types: ["code"],
subject_type: "public",
contacts: contacts
}, {
silent: true
});
} else {
client.set({
require_auth_time: true,
default_max_age: 60000,
scope: _.uniq(_.flatten(app.systemScopeList.defaultUnrestrictedScopes().pluck("value"))).join(" "),
token_endpoint_auth_method: 'client_secret_basic',
grant_types: ["authorization_code"],
response_types: ["code"],
subject_type: "public",
contacts: contacts
}, {
silent: true
});
}
$('#content').html(view.render().el);
view.delegateEvents();
setPageTitle($.t('dynreg.new-client'));
});
}
});
ui.routes.push({
path: "dev/dynreg/edit",
name: "editDynReg",
callback: function() {
this.breadCrumbView.collection.reset();
this.breadCrumbView.collection.add([{
text: $.t('admin.home'),
href: ""
}, {
text: $.t('admin.self-service-client'),
href: "manage/#dev/dynreg"
}, {
text: $.t('dynreg.edit-existing'),
href: "manage/#dev/dynreg/edit"
}]);
this.updateSidebar('dev/dynreg');
setPageTitle($.t('dynreg.edit-existing'));
// note that this doesn't actually load the client, that's supposed to
// happen elsewhere...
}
});
ui.templates.push('resources/template/dynreg.html');