diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag index 43790a655..b0cb9869b 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/actionmenu.tag @@ -15,4 +15,5 @@
  • View Profile Information
  • -
  • Self-service client registration
  • \ No newline at end of file +
  • Self-service client registration
  • +
  • Self-service protected resource registration
  • \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag index f59f21d2f..0e7ec53aa 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/tags/footer.tag @@ -29,6 +29,7 @@ + diff --git a/openid-connect-server-webapp/src/main/webapp/resources/js/admin.js b/openid-connect-server-webapp/src/main/webapp/resources/js/admin.js index ae819d617..a40b93aab 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/js/admin.js +++ b/openid-connect-server-webapp/src/main/webapp/resources/js/admin.js @@ -426,6 +426,10 @@ var AppRouter = Backbone.Router.extend({ "dev/dynreg/new":"newDynReg", "dev/dynreg/edit":"editDynReg", + "dev/resource":"resReg", + "dev/resource/new":"newResReg", + "dev/resource/edit":"editResReg", + "": "root" }, @@ -456,6 +460,7 @@ var AppRouter = Backbone.Router.extend({ this.systemScopeListView = new SystemScopeListView({model:this.systemScopeList}); this.tokensListView = new TokenListView({model: {access: this.accessTokensList, refresh: this.refreshTokensList}, clientList: this.clientList, systemScopeList: this.systemScopeList}); this.dynRegRootView = new DynRegRootView({systemScopeList: this.systemScopeList}); + this.resRegRootView = new ResRegRootView({systemScopeList: this.systemScopeList}); this.breadCrumbView = new BreadCrumbView({ collection:new Backbone.Collection() @@ -893,13 +898,66 @@ var AppRouter = Backbone.Router.extend({ this.breadCrumbView.collection.add([ {text:"Home", href:""}, {text:"Client Registration", href:"manage/#dev/dynreg"}, - {text:"Edit", href:"manage/#dev/dynreg/new"} + {text:"Edit", href:"manage/#dev/dynreg/edit"} ]); setPageTitle("Edit a Dynamically Registered Client"); // note that this doesn't actually load the client, that's supposed to happen elsewhere... }, + resReg:function() { + this.breadCrumbView.collection.reset(); + this.breadCrumbView.collection.add([ + {text:"Home", href:""}, + {text:"Protected Resource Registration", href:"manage/#dev/resource"} + ]); + + this.resRegRootView.load(function() { + $('#content').html(app.resRegRootView.render().el); + + setPageTitle("Self-service Protected Resource Registration"); + }); + + }, + + newResReg:function() { + this.breadCrumbView.collection.reset(); + this.breadCrumbView.collection.add([ + {text:"Home", href:""}, + {text:"Protected Resource Registration", href:"manage/#dev/resource"}, + {text:"New", href:"manage/#dev/resource/new"} + ]); + + var client = new ResRegClient(); + var view = new ResRegEditView({model: client, systemScopeList:this.systemScopeList}); + + view.load(function() { + + client.set({ + scope: _.uniq(_.flatten(app.systemScopeList.defaultDynRegScopes().pluck("value"))).join(" "), + token_endpoint_auth_method: 'client_secret_basic', + }, { silent: true }); + + $('#content').html(view.render().el); + view.delegateEvents(); + setPageTitle("Dynamically Register a New Protected Resource"); + + }); + + }, + + editResReg:function() { + this.breadCrumbView.collection.reset(); + this.breadCrumbView.collection.add([ + {text:"Home", href:""}, + {text:"Protected Resource Registration", href:"manage/#dev/resource"}, + {text:"Edit", href:"manage/#dev/resource/edit"} + ]); + + setPageTitle("Edit a Dynamically Registered Protected Resource"); + // note that this doesn't actually load the client, that's supposed to happen elsewhere... + }, + profile:function() { this.breadCrumbView.collection.reset(); this.breadCrumbView.collection.add([ @@ -937,6 +995,7 @@ $(function () { $.get('resources/template/scope.html', _load); $.get('resources/template/whitelist.html', _load); $.get('resources/template/dynreg.html', _load); + $.get('resources/template/rsreg.html', _load); $.get('resources/template/token.html', _load); jQuery.ajaxSetup({async:true}); diff --git a/openid-connect-server-webapp/src/main/webapp/resources/js/rsreg.js b/openid-connect-server-webapp/src/main/webapp/resources/js/rsreg.js new file mode 100644 index 000000000..b3375f93f --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/resources/js/rsreg.js @@ -0,0 +1,458 @@ +var ResRegClient = 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, + + 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_uri:null, + + request_uris:[], + + client_description: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:'resource' + +}); + +var ResRegRootView = Backbone.View.extend({ + + tagName: 'span', + + initialize:function() { + + }, + + events:{ + "click #newreg":"newReg", + "click #editreg":"editReg" + }, + + load:function(callback) { + if (this.options.systemScopeList.isFetched) { + callback(); + return; + } + + $('#loadingbox').sheet('show'); + $('#loading').html('Scopes '); + + $.when(this.options.systemScopeList.fetchIfNeeded({success:function(e) {$('#loading-scopes').addClass('label-success');}})) + .done(function() { + $('#loadingbox').sheet('hide'); + callback(); + }); + }, + + render:function() { + $(this.el).html($('#tmpl-rsreg').html()); + return this; + }, + + newReg:function(e) { + e.preventDefault(); + this.remove(); + app.navigate('dev/resource/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 view = new ResRegEditView({model: client, systemScopeList: app.systemScopeList}); + + view.load(function() { + $('#content').html(view.render().el); + view.delegateEvents(); + setPageTitle("Dynamically Register a New Protected Resource"); + app.navigate('dev/resource/edit', {trigger: true}); + self.remove(); + }); + }, error: function() { + $('#modalAlert div.modal-body').html("Invalid resource or registration access token."); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + + }}); + } + +}); + +var ResRegEditView = Backbone.View.extend({ + + tagName: 'span', + + initialize:function() { + if (!this.template) { + this.template = _.template($('#tmpl-rsreg-resource-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(); + }, + + load:function(callback) { + if (this.options.systemScopeList.isFetched) { + callback(); + return; + } + + $('#loadingbox').sheet('show'); + $('#loading').html('Scopes '); + + $.when(this.options.systemScopeList.fetchIfNeeded({success:function(e) {$('#loading-scopes').addClass('label-success');}})) + .done(function() { + $('#loadingbox').sheet('hide'); + callback(); + }); + }, + + events:{ + "click .btn-save":"saveClient", + "click .btn-cancel": function() { window.history.back(); return false; }, + "click .btn-delete":"deleteClient", + "change #logoUri input":"previewLogo" + }, + + deleteClient:function (e) { + e.preventDefault(); + + if (confirm("Are you sure sure you would like to delete this client?")) { + var self = this; + + this.model.destroy({ + success:function () { + self.remove(); + app.navigate('dev/dynreg', {trigger: true}); + }, + error:function (error, response) { + console.log("An error occurred when deleting a client"); + + //Pull out the response text. + var responseJson = JSON.parse(response.responseText); + + //Display an alert with an error message + $('#modalAlert div.modal-header').html(responseJson.error); + $('#modalAlert div.modal-body').html(responseJson.error_description); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + } + }); + + app.clientListView.delegateEvents(); + } + + return false; + }, + + previewLogo:function() { + if ($('#logoUri input', this.el).val()) { + $('#logoPreview', this.el).empty(); + $('#logoPreview', this.el).attr('src', $('#logoUri input').val()); + } else { + $('#logoBlock', 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; + } + }, + + // 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'); + + // 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); + } + } + + var attrs = { + client_name:$('#clientName input').val(), + redirect_uris: this.redirectUrisCollection.pluck("item"), + client_description:$('#clientDescription textarea').val(), + logo_uri:$('#logoUri input').val(), + grant_types: grantTypes, + scope: scopes, + + tos_uri: $('#tosUri input').val(), + policy_uri: $('#policyUri input').val(), + client_uri: $('#clientUri input').val(), + application_type: $('#applicationType input').filter(':checked').val(), + jwks_uri: $('#jwksUri input').val(), + subject_type: $('#subjectType input').filter(':checked').val(), + token_endpoint_auth_method: $('#tokenEndpointAuthMethod input').filter(':checked').val(), + response_types: responseTypes, + sector_identifier_uri: $('#sectorIdentifierUri input').val(), + initiate_login_uri: $('#initiateLoginUri input').val(), + post_logout_redirect_uri: $('#postLogoutRedirectUri input').val(), + reuse_refresh_token: $('#reuseRefreshToken').is(':checked'), + 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()) + }; + + // 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 view = new DynRegEditView({model: _self.model, systemScopeList: _self.options.systemScopeList}); + + view.load(function() { + // reload + $('#content').html(view.render().el); + view.delegateEvents(); + }); + }, + error:function (error, response) { + console.log("An error occurred when deleting from a list widget"); + + //Pull out the response text. + var responseJson = JSON.parse(response.responseText); + + //Display an alert with an error message + $('#modalAlert div.modal-header').html(responseJson.error); + $('#modalAlert div.modal-body').html(responseJson.error_description); + + $("#modalAlert").modal({ // wire up the actual modal functionality and show the dialog + "backdrop" : "static", + "keyboard" : true, + "show" : true // ensure the modal is shown immediately + }); + } + }); + + return false; + }, + + render:function() { + $(this.el).html(this.template({client: this.model.toJSON()})); + + var _self = this; + + // build and bind registered redirect URI collection and view + _.each(this.model.get("redirectUris"), function (redirectUri) { + _self.redirectUrisCollection.add(new URIModel({item:redirectUri})); + }); + + $("#redirectUris .controls",this.el).html(new ListWidgetView({ + type:'uri', + placeholder: 'https://', + collection: this.redirectUrisCollection}).render().el); + + // 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})); + }); + + $("#scope .controls",this.el).html(new ListWidgetView({ + placeholder: 'new scope', + autocomplete: _.uniq(_.flatten(this.options.systemScopeList.pluck("value"))), + collection: this.scopeCollection}).render().el); + + // build and bind contacts + _.each(this.model.get('contacts'), function (contact) { + _self.contactsCollection.add(new Backbone.Model({item:contact})); + }); + + $("#contacts .controls", this.el).html(new ListWidgetView({ + placeholder: 'new contact', + collection: this.contactsCollection}).render().el); + + + // build and bind request URIs + _.each(this.model.get('requestUris'), function (requestUri) { + _self.requestUrisCollection.add(new URIModel({item:requestUri})); + }); + + $('#requestUris .controls', this.el).html(new ListWidgetView({ + type: 'uri', + placeholder: 'https://', + collection: this.requestUrisCollection}).render().el); + + // build and bind default ACR values + _.each(this.model.get('defaultAcrValues'), function (defaultAcrValue) { + _self.defaultAcrValuesCollection.add(new Backbone.Model({item:defaultAcrValue})); + }); + + $('#defaultAcrValues .controls', this.el).html(new ListWidgetView({ + placeholder: 'new ACR value', + // TODO: autocomplete from spec + collection: this.defaultAcrValuesCollection}).render().el); + + this.previewLogo(); + + // 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: 'Not Yet Implemented', + content: 'The value of this field will be saved with the client, ' + +'but the server does not currently process anything with it. ' + +'Future versions of the server library will make use of this.' + }); + + + return this; + } + +}); \ No newline at end of file diff --git a/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html b/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html index 9f261a629..a5c8638dc 100644 --- a/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html +++ b/openid-connect-server-webapp/src/main/webapp/resources/template/dynreg.html @@ -67,7 +67,7 @@
    - Warning! You MUST protect your your Client ID, Client Secret, and your Registration Access Token. If + Warning! You MUST protect your Client ID, Client Secret, and your Registration Access Token. If you lose your Client ID or Registration Access Token, you will no longer have access to your client's registration records and you will need to register a new client.
    diff --git a/openid-connect-server-webapp/src/main/webapp/resources/template/rsreg.html b/openid-connect-server-webapp/src/main/webapp/resources/template/rsreg.html new file mode 100644 index 000000000..3a3b90696 --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/resources/template/rsreg.html @@ -0,0 +1,301 @@ + + + + + + +