diff --git a/src/Components/Web.JS/dist/Release/blazor.webassembly.js b/src/Components/Web.JS/dist/Release/blazor.webassembly.js index 7a3ffda018..a0d338fd37 100644 --- a/src/Components/Web.JS/dist/Release/blazor.webassembly.js +++ b/src/Components/Web.JS/dist/Release/blazor.webassembly.js @@ -1 +1 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=44)}([,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(18);var r=n(26),o=n(13),a={},i=!1;function u(e,t,n){var o=a[e];o||(o=a[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=u,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(e);if(!r)throw new Error("Could not find any element matching selector '"+e+"'.");u(n||0,o.toLogicalElement(r,!0),t)},t.renderBatch=function(e,t){var n=a[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),u=r.values(o),s=r.count(o),c=t.referenceFrames(),l=r.values(c),f=t.diffReader,d=0;d0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(c(a)&&c(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(s(a))throw new Error("Not implemented: moving existing logical children");var i=c(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=s,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return c(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=c,t.permuteLogicalChildren=function(e,t){var n=c(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=s(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},,,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,a=null;function i(e){t.push(e)}function u(e,t,n,r){var o=c();if(o.invokeDotNetFromJS){var a=JSON.stringify(r,m),i=o.invokeDotNetFromJS(e,t,n,a);return i?f(i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.")}function s(e,t,r,a){if(e&&r)throw new Error("For instance method calls, assemblyName should be null. Received '"+e+"'.");var i=o++,u=new Promise(function(e,t){n[i]={resolve:e,reject:t}});try{var s=JSON.stringify(a,m);c().beginInvokeDotNetFromJS(i,e,t,r,s)}catch(e){l(i,!1,e)}return u}function c(){if(null!==a)return a;throw new Error("No .NET call dispatcher has been set.")}function l(e,t,r){if(!n.hasOwnProperty(e))throw new Error("There is no pending async call with ID "+e+".");var o=n[e];delete n[e],t?o.resolve(r):o.reject(r)}function f(e){return e?JSON.parse(e,function(e,n){return t.reduce(function(t,n){return n(e,t)},n)}):null}function d(e){return e instanceof Error?e.message+"\n"+e.stack:e?e.toString():"null"}function p(e){if(r.hasOwnProperty(e))return r[e];var t,n=window,o="window";if(e.split(".").forEach(function(e){if(!(e in n))throw new Error("Could not find '"+e+"' in '"+o+"'.");t=n,n=n[e],o+="."+e}),n instanceof Function)return n=n.bind(t),r[e]=n,n;throw new Error("The value '"+o+"' is not a function.")}e.attachDispatcher=function(e){a=e},e.attachReviver=i,e.invokeMethod=function(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0)&&!(r=a.next()).done;)i.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=a.return)&&n.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(t,"__esModule",{value:!0}),n(17),n(24);var i=n(18),u=n(45),s=n(5),c=n(47),l=n(33),f=n(19),d=n(48),p=n(49),h=n(50),m=!1;function v(e){return r(this,void 0,void 0,function(){var e,t,n,l,v,y=this;return o(this,function(b){switch(b.label){case 0:if(m)throw new Error("Blazor has already started.");return m=!0,f.setEventDispatcher(function(e,t){return DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","DispatchEvent",e,JSON.stringify(t))}),e=i.setPlatform(u.monoPlatform),window.Blazor.platform=e,window.Blazor._internal.renderBatch=function(e,t){s.renderBatch(e,new c.SharedMemoryRenderBatch(t))},window.Blazor._internal.navigationManager.listenForNavigationEvents(function(e,t){return r(y,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return[4,DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChanged",e,t)];case 1:return n.sent(),[2]}})})}),[4,h.BootConfigResult.initAsync()];case 1:return t=b.sent(),[4,Promise.all([d.WebAssemblyResourceLoader.initAsync(t.bootConfig),p.WebAssemblyConfigLoader.initAsync(t)])];case 2:n=a.apply(void 0,[b.sent(),1]),l=n[0],b.label=3;case 3:return b.trys.push([3,5,,6]),[4,e.start(l)];case 4:return b.sent(),[3,6];case 5:throw v=b.sent(),new Error("Failed to start platform. Reason: "+v);case 6:return e.callEntryPoint(l.bootConfig.entryAssembly),[2]}})})}window.Blazor.start=v,l.shouldAutoStart()&&v().catch(function(e){"undefined"!=typeof Module&&Module.printErr?Module.printErr(e):console.error(e)})},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{s(r.next(e))}catch(e){a(e)}}function u(e){try{s(r.throw(e))}catch(e){a(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]>2,r=Module.HEAPU32[n+1];if(r>l)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*c+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,t,n){var r=Module.getValue(e+(t||0),"i32");if(0===r)return null;if(n){var o=BINDING.unbox_mono_obj(r);return"boolean"==typeof o?o?"":null:o}return BINDING.conv_string(r)},readStructField:function(e,t){return e+(t||0)}};var f=document.createElement("a");function d(e){return e+12}function p(e,t,n){var r="["+e+"] "+t+":"+n;return BINDING.bind_static_method(r)}function h(e,t){return r(this,void 0,void 0,function(){var n,r;return o(this,function(o){switch(o.label){case 0:if("function"!=typeof WebAssembly.instantiateStreaming)return[3,4];o.label=1;case 1:return o.trys.push([1,3,,4]),[4,WebAssembly.instantiateStreaming(e.response,t)];case 2:return[2,o.sent().instance];case 3:return n=o.sent(),console.info("Streaming compilation failed. Falling back to ArrayBuffer instantiation. ",n),[3,4];case 4:return[4,e.response.then(function(e){return e.arrayBuffer()})];case 5:return r=o.sent(),[4,WebAssembly.instantiate(r,t)];case 6:return[2,o.sent().instance]}})})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=window.chrome&&navigator.userAgent.indexOf("Edge")<0,o=!1;function a(){return o&&r}t.hasDebuggingEnabled=a,t.attachDebuggerHotkey=function(e){var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";a()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),o=!!e.bootConfig.resources.pdb,document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(o?r?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Edge(Chromium) or Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(18),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=a,this.arrayBuilderSegmentReader=i,this.diffReader=u,this.editReader=s,this.frameReader=c}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,a.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*a.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*a.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return l(e,t,u.structLength)},e.prototype.referenceFramesEntry=function(e,t){return l(e,t,c.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=l(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=l(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var a={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},i={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},u={structLength:4+i.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return l(e,t,s.structLength)}},s={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},c={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24,!0)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function l(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{s(r.next(e))}catch(e){a(e)}}function u(e){try{s(r.throw(e))}catch(e){a(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(c(a)&&c(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(s(a))throw new Error("Not implemented: moving existing logical children");var i=c(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=s,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return c(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=c,t.permuteLogicalChildren=function(e,t){var n=c(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=s(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},,,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,a=null;function i(e){t.push(e)}function u(e,t,n,r){var o=c();if(o.invokeDotNetFromJS){var a=JSON.stringify(r,m),i=o.invokeDotNetFromJS(e,t,n,a);return i?f(i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.")}function s(e,t,r,a){if(e&&r)throw new Error("For instance method calls, assemblyName should be null. Received '"+e+"'.");var i=o++,u=new Promise(function(e,t){n[i]={resolve:e,reject:t}});try{var s=JSON.stringify(a,m);c().beginInvokeDotNetFromJS(i,e,t,r,s)}catch(e){l(i,!1,e)}return u}function c(){if(null!==a)return a;throw new Error("No .NET call dispatcher has been set.")}function l(e,t,r){if(!n.hasOwnProperty(e))throw new Error("There is no pending async call with ID "+e+".");var o=n[e];delete n[e],t?o.resolve(r):o.reject(r)}function f(e){return e?JSON.parse(e,function(e,n){return t.reduce(function(t,n){return n(e,t)},n)}):null}function d(e){return e instanceof Error?e.message+"\n"+e.stack:e?e.toString():"null"}function p(e){if(r.hasOwnProperty(e))return r[e];var t,n=window,o="window";if(e.split(".").forEach(function(e){if(!(e in n))throw new Error("Could not find '"+e+"' in '"+o+"'.");t=n,n=n[e],o+="."+e}),n instanceof Function)return n=n.bind(t),r[e]=n,n;throw new Error("The value '"+o+"' is not a function.")}e.attachDispatcher=function(e){a=e},e.attachReviver=i,e.invokeMethod=function(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0)&&!(r=a.next()).done;)i.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=a.return)&&n.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(t,"__esModule",{value:!0}),n(17),n(24);var i=n(18),u=n(45),s=n(5),c=n(47),l=n(33),f=n(19),d=n(48),p=n(49),h=n(50),m=!1;function v(e){return r(this,void 0,void 0,function(){var e,t,n,l,v,y=this;return o(this,function(b){switch(b.label){case 0:if(m)throw new Error("Blazor has already started.");return m=!0,f.setEventDispatcher(function(e,t){return DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","DispatchEvent",e,JSON.stringify(t))}),e=i.setPlatform(u.monoPlatform),window.Blazor.platform=e,window.Blazor._internal.renderBatch=function(e,t){s.renderBatch(e,new c.SharedMemoryRenderBatch(t))},window.Blazor._internal.navigationManager.listenForNavigationEvents(function(e,t){return r(y,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return[4,DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChanged",e,t)];case 1:return n.sent(),[2]}})})}),[4,h.BootConfigResult.initAsync()];case 1:return t=b.sent(),[4,Promise.all([d.WebAssemblyResourceLoader.initAsync(t.bootConfig),p.WebAssemblyConfigLoader.initAsync(t)])];case 2:n=a.apply(void 0,[b.sent(),1]),l=n[0],b.label=3;case 3:return b.trys.push([3,5,,6]),[4,e.start(l)];case 4:return b.sent(),[3,6];case 5:throw v=b.sent(),new Error("Failed to start platform. Reason: "+v);case 6:return e.callEntryPoint(l.bootConfig.entryAssembly),[2]}})})}window.Blazor.start=v,l.shouldAutoStart()&&v().catch(function(e){"undefined"!=typeof Module&&Module.printErr?Module.printErr(e):console.error(e)})},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{s(r.next(e))}catch(e){a(e)}}function u(e){try{s(r.throw(e))}catch(e){a(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]>2,r=Module.HEAPU32[n+1];if(r>l)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*c+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,t,n){var r=Module.getValue(e+(t||0),"i32");if(0===r)return null;if(n){var o=BINDING.unbox_mono_obj(r);return"boolean"==typeof o?o?"":null:o}return BINDING.conv_string(r)},readStructField:function(e,t){return e+(t||0)}};var f=document.createElement("a");function d(e){return e+12}function p(e,t,n){var r="["+e+"] "+t+":"+n;return BINDING.bind_static_method(r)}function h(e,t){return r(this,void 0,void 0,function(){var n,r;return o(this,function(o){switch(o.label){case 0:if("function"!=typeof WebAssembly.instantiateStreaming)return[3,4];o.label=1;case 1:return o.trys.push([1,3,,4]),[4,WebAssembly.instantiateStreaming(e.response,t)];case 2:return[2,o.sent().instance];case 3:return n=o.sent(),console.info("Streaming compilation failed. Falling back to ArrayBuffer instantiation. ",n),[3,4];case 4:return[4,e.response.then(function(e){return e.arrayBuffer()})];case 5:return r=o.sent(),[4,WebAssembly.instantiate(r,t)];case 6:return[2,o.sent().instance]}})})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=window.chrome&&navigator.userAgent.indexOf("Edge")<0,o=!1;function a(){return o&&r}t.hasDebuggingEnabled=a,t.attachDebuggerHotkey=function(e){var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";a()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),o=!!e.bootConfig.resources.pdb,document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(o?r?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Edge(Chromium) or Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(18),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=a,this.arrayBuilderSegmentReader=i,this.diffReader=u,this.editReader=s,this.frameReader=c}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,a.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*a.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*a.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return l(e,t,u.structLength)},e.prototype.referenceFramesEntry=function(e,t){return l(e,t,c.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=l(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=l(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var a={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},i={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},u={structLength:4+i.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return l(e,t,s.structLength)}},s={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},c={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24,!0)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function l(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{s(r.next(e))}catch(e){a(e)}}function u(e){try{s(r.throw(e))}catch(e){a(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1] - public interface IRemoteAuthenticationPathsProvider + internal interface IRemoteAuthenticationPathsProvider { /// /// This is an internal API that supports the Microsoft.AspNetCore.Components.WebAssembly.Authentication diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/AccessToken.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/AccessToken.cs index 0b6f831fda..a282d1c794 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/AccessToken.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/AccessToken.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication { @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// Gets or sets the list of granted scopes for the token. /// - public string[] GrantedScopes { get; set; } + public IReadOnlyList GrantedScopes { get; set; } /// /// Gets the expiration time of the token. diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/RemoteAuthenticationResult.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/RemoteAuthenticationResult.cs index 4af828b37f..529f34e54b 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/RemoteAuthenticationResult.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Models/RemoteAuthenticationResult.cs @@ -6,8 +6,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// Represents the result of an authentication operation. /// - /// The type of the preserved state during the authentication operation. - public class RemoteAuthenticationResult where TState : class + /// The type of the preserved state during the authentication operation. + public class RemoteAuthenticationResult where TRemoteAuthenticationState : RemoteAuthenticationState { /// /// Gets or sets the status of the authentication operation. The status can be one of . @@ -22,6 +22,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// Gets or sets the preserved state of a successful authentication operation. /// - public TState State { get; set; } + public TRemoteAuthenticationState State { get; set; } } } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/OidcProviderOptions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/OidcProviderOptions.cs index 56859689ed..dbb43e1dbf 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/OidcProviderOptions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/OidcProviderOptions.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// Gets or sets the list of scopes to request when signing in. /// - public IList DefaultScopes { get; set; } = new List { "openid", "profile" }; + public IList DefaultScopes { get; } = new List { "openid", "profile" }; /// /// Gets or sets the redirect uri for the application. The application will be redirected here after the user has completed the sign in diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/RemoteAuthenticationOptions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/RemoteAuthenticationOptions.cs index 7efb26f44c..b20c2f8b26 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/RemoteAuthenticationOptions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/RemoteAuthenticationOptions.cs @@ -12,16 +12,16 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// Gets or sets the provider options. /// - public TRemoteAuthenticationProviderOptions ProviderOptions { get; set; } = new TRemoteAuthenticationProviderOptions(); + public TRemoteAuthenticationProviderOptions ProviderOptions { get; } = new TRemoteAuthenticationProviderOptions(); /// /// Gets or sets the . /// - public RemoteAuthenticationApplicationPathsOptions AuthenticationPaths { get; set; } = new RemoteAuthenticationApplicationPathsOptions(); + public RemoteAuthenticationApplicationPathsOptions AuthenticationPaths { get; } = new RemoteAuthenticationApplicationPathsOptions(); /// /// Gets or sets the . /// - public RemoteAuthenticationUserOptions UserOptions { get; set; } = new RemoteAuthenticationUserOptions(); + public RemoteAuthenticationUserOptions UserOptions { get; } = new RemoteAuthenticationUserOptions(); } } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationBuilderExtensions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationBuilderExtensions.cs index 29645fc671..471b8cef7a 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationBuilderExtensions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationBuilderExtensions.cs @@ -12,44 +12,44 @@ namespace Microsoft.Extensions.DependencyInjection public static class RemoteAuthenticationBuilderExtensions { /// - /// Replaces the existing with the user factory defined by . + /// Replaces the existing with the user factory defined by . /// /// The remote authentication state. /// The account type. - /// The new user factory type. + /// The new user factory type. /// The . /// The . - public static IRemoteAuthenticationBuilder AddUserFactory( + public static IRemoteAuthenticationBuilder AddAccountClaimsPrincipalFactory( this IRemoteAuthenticationBuilder builder) where TRemoteAuthenticationState : RemoteAuthenticationState, new() where TAccount : RemoteUserAccount - where TUserFactory : AccountClaimsPrincipalFactory + where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory { - builder.Services.Replace(ServiceDescriptor.Scoped, TUserFactory>()); + builder.Services.Replace(ServiceDescriptor.Scoped, TAccountClaimsPrincipalFactory>()); return builder; } /// - /// Replaces the existing with the user factory defined by . + /// Replaces the existing with the user factory defined by . /// /// The remote authentication state. - /// The new user factory type. + /// The new user factory type. /// The . /// The . - public static IRemoteAuthenticationBuilder AddUserFactory( + public static IRemoteAuthenticationBuilder AddAccountClaimsPrincipalFactory( this IRemoteAuthenticationBuilder builder) where TRemoteAuthenticationState : RemoteAuthenticationState, new() - where TUserFactory : AccountClaimsPrincipalFactory => builder.AddUserFactory(); + where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory => builder.AddAccountClaimsPrincipalFactory(); /// - /// Replaces the existing with the user factory defined by . + /// Replaces the existing with the user factory defined by . /// - /// The new user factory type. + /// The new user factory type. /// The . /// The . - public static IRemoteAuthenticationBuilder AddUserFactory( + public static IRemoteAuthenticationBuilder AddAccountClaimsPrincipalFactory( this IRemoteAuthenticationBuilder builder) - where TUserFactory : AccountClaimsPrincipalFactory => builder.AddUserFactory(); + where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory => builder.AddAccountClaimsPrincipalFactory(); } } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs index 54016a4d1c..51d66c7a9e 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs @@ -11,46 +11,46 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// The default login path. /// - public const string LoginPath = "authentication/login"; + public static readonly string LoginPath = "authentication/login"; /// /// The default login callback path. /// - public const string LoginCallbackPath = "authentication/login-callback"; + public static readonly string LoginCallbackPath = "authentication/login-callback"; /// /// The default login failed path. /// - public const string LoginFailedPath = "authentication/login-failed"; + public static readonly string LoginFailedPath = "authentication/login-failed"; /// /// The default logout path. /// - public const string LogoutPath = "authentication/logout"; + public static readonly string LogoutPath = "authentication/logout"; /// /// The default logout callback path. /// - public const string LogoutCallbackPath = "authentication/logout-callback"; + public static readonly string LogoutCallbackPath = "authentication/logout-callback"; /// /// The default logout failed path. /// - public const string LogoutFailedPath = "authentication/logout-failed"; + public static readonly string LogoutFailedPath = "authentication/logout-failed"; /// /// The default logout succeeded path. /// - public const string LogoutSucceededPath = "authentication/logged-out"; + public static readonly string LogoutSucceededPath = "authentication/logged-out"; /// /// The default profile path. /// - public const string ProfilePath = "authentication/profile"; + public static readonly string ProfilePath = "authentication/profile"; /// /// The default register path. /// - public const string RegisterPath = "authentication/register"; + public static readonly string RegisterPath = "authentication/register"; } } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.cs index 47582a2b7a..6d76fd0247 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticatorViewCore.cs @@ -88,34 +88,34 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// Gets or sets the to use for performin JavaScript interop. /// - [Inject] public IJSRuntime JS { get; set; } + [Inject] internal IJSRuntime JS { get; set; } /// /// Gets or sets the to use for redirecting the browser. /// - [Inject] public NavigationManager Navigation { get; set; } + [Inject] internal NavigationManager Navigation { get; set; } /// /// Gets or sets the to use for handling the underlying authentication protocol. /// - [Inject] public IRemoteAuthenticationService AuthenticationService { get; set; } + [Inject] internal IRemoteAuthenticationService AuthenticationService { get; set; } /// /// Gets or sets a default to use as fallback if an has not been explicitly specified. /// #pragma warning disable PUB0001 // Pubternal type in public API - [Inject] public IRemoteAuthenticationPathsProvider RemoteApplicationPathsProvider { get; set; } + [Inject] internal IRemoteAuthenticationPathsProvider RemoteApplicationPathsProvider { get; set; } #pragma warning restore PUB0001 // Pubternal type in public API /// /// Gets or sets a default with the current user. /// - [Inject] public AuthenticationStateProvider AuthenticationProvider { get; set; } + [Inject] internal AuthenticationStateProvider AuthenticationProvider { get; set; } /// /// Gets or sets a default with the current user. /// - [Inject] public SignOutSessionStateManager SignOutManager { get; set; } + [Inject] internal SignOutSessionStateManager SignOutManager { get; set; } /// /// Gets or sets the with the paths to different authentication pages. diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenRequestOptions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenRequestOptions.cs index 235f6c3855..cffb2fd3dc 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenRequestOptions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenRequestOptions.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// /// Gets or sets the list of scopes to request for the token. /// - public string[] Scopes { get; set; } + public IEnumerable Scopes { get; set; } /// /// Gets or sets a specific return url to use for returning the user back to the application if it needs to be diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs index db1bd6faf9..8fe4a707b0 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs @@ -11,32 +11,29 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication public class AccessTokenResult { private readonly AccessToken _token; - private readonly NavigationManager _navigation; /// /// Initializes a new instance of . /// /// The status of the result. /// The in case it was successful. - /// The to perform redirects. /// The redirect uri to go to for provisioning the token. - public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, NavigationManager navigation, string redirectUrl) + public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, string redirectUrl) { Status = status; _token = token; - _navigation = navigation; RedirectUrl = redirectUrl; } /// /// Gets or sets the status of the current operation. See for a list of statuses. /// - public AccessTokenResultStatus Status { get; set; } + public AccessTokenResultStatus Status { get; } /// /// Gets or sets the URL to redirect to if is . /// - public string RedirectUrl { get; set; } + public string RedirectUrl { get; } /// /// Determines whether the token request was successful and makes the available for use when it is. @@ -56,27 +53,5 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication return false; } } - - /// - /// Determines whether the token request was successful and makes the available for use when it is. - /// - /// The if the request was successful. - /// Whether or not to redirect automatically when failing to provision a token. - /// true when the token request is successful; false otherwise. - public bool TryGetToken(out AccessToken accessToken, bool redirect) - { - if (TryGetToken(out accessToken)) - { - return true; - } - else - { - if (redirect) - { - _navigation.NavigateTo(RedirectUrl); - } - return false; - } - } } } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs new file mode 100644 index 0000000000..ad019ff080 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication +{ + /// + /// A that attaches access tokens to outgoing instances. + /// Access tokens will only be added when the request URI is within one of the base addresses configured using + /// . + /// + public class AuthorizationMessageHandler : DelegatingHandler + { + private readonly IAccessTokenProvider _provider; + private readonly NavigationManager _navigation; + private AccessToken _lastToken; + private AuthenticationHeaderValue _cachedHeader; + private Uri[] _authorizedUris; + private AccessTokenRequestOptions _tokenOptions; + + /// + /// Initializes a new instance of . + /// + /// The to use for provisioning tokens. + /// The to use for performing redirections. + public AuthorizationMessageHandler( + IAccessTokenProvider provider, + NavigationManager navigation) + { + _provider = provider; + _navigation = navigation; + } + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var now = DateTimeOffset.Now; + if (_authorizedUris == null) + { + throw new InvalidOperationException($"The '{nameof(AuthorizationMessageHandler)}' is not configured. " + + $"Call '{nameof(AuthorizationMessageHandler.ConfigureHandler)}' and provide a list of endpoint urls to attach the token to."); + } + + if (_authorizedUris.Any(uri => uri.IsBaseOf(request.RequestUri))) + { + if (_lastToken == null || now >= _lastToken.Expires.AddMinutes(-5)) + { + var tokenResult = _tokenOptions != null ? + await _provider.RequestAccessToken(_tokenOptions) : + await _provider.RequestAccessToken(); + + if (tokenResult.TryGetToken(out var token)) + { + _lastToken = token; + _cachedHeader = new AuthenticationHeaderValue("Bearer", _lastToken.Value); + } + else + { + throw new AccessTokenNotAvailableException(_navigation, tokenResult, _tokenOptions?.Scopes); + } + } + + // We don't try to handle 401s and retry the request with a new token automatically since that would mean we need to copy the request + // headers and buffer the body and we expect that the user instead handles the 401s. (Also, we can't really handle all 401s as we might + // not be able to provision a token without user interaction). + request.Headers.Authorization = _cachedHeader; + } + + return await base.SendAsync(request, cancellationToken); + } + + /// + /// Configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if only attached if at least one of + /// is a base of . + /// + /// The base addresses of endpoint URLs to which the token will be attached. + /// The list of scopes to use when requesting an access token. + /// The return URL to use in case there is an issue provisioning the token and a redirection to the + /// identity provider is necessary. + /// + /// This . + public AuthorizationMessageHandler ConfigureHandler( + IEnumerable authorizedUrls, + IEnumerable scopes = null, + string returnUrl = null) + { + if (_authorizedUris != null) + { + throw new InvalidOperationException("Handler already configured."); + } + + if (authorizedUrls == null) + { + throw new ArgumentNullException(nameof(authorizedUrls)); + } + + var uris = authorizedUrls.Select(uri => new Uri(uri, UriKind.Absolute)).ToArray(); + if (uris.Length == 0) + { + throw new ArgumentException("At least one URL must be configured.", nameof(authorizedUrls)); + } + + _authorizedUris = uris; + var scopesList = scopes?.ToArray(); + if (scopesList != null || returnUrl != null) + { + _tokenOptions = new AccessTokenRequestOptions + { + Scopes = scopesList, + ReturnUrl = returnUrl + }; + } + + return this; + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/BaseAddressAuthorizationMessageHandler.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/BaseAddressAuthorizationMessageHandler.cs new file mode 100644 index 0000000000..5fe5511f1e --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/BaseAddressAuthorizationMessageHandler.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication +{ + /// + /// A that attaches access tokens to outgoing instances. + /// Access tokens will only be added when the request URI is within the application's base URI. + /// + public class BaseAddressAuthorizationMessageHandler : AuthorizationMessageHandler + { + /// + /// Initializes a new instance of . + /// + /// The to use for requesting tokens. + /// The used to compute the base address. + public BaseAddressAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigationManager) + : base(provider, navigationManager) + { + ConfigureHandler(new[] { navigationManager.BaseUri }); + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs new file mode 100644 index 0000000000..762d4de429 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication +{ + /// + /// An that is thrown when an instance + /// is not able to provision an access token. + /// + public class AccessTokenNotAvailableException : Exception + { + private readonly NavigationManager _navigation; + private readonly AccessTokenResult _tokenResult; + + public AccessTokenNotAvailableException( + NavigationManager navigation, + AccessTokenResult tokenResult, + IEnumerable scopes) + : base(message: "Unable to provision an access token for the requested scopes: " + + scopes != null ? $"'{string.Join(", ", scopes ?? Array.Empty())}'" : "(default scopes)") + { + _tokenResult = tokenResult; + _navigation = navigation; + } + + public void Redirect() => _navigation.NavigateTo(_tokenResult.RedirectUrl); + } +} diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs index 00fcc8ba2d..30e07e27ba 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs @@ -43,9 +43,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication protected NavigationManager Navigation { get; } /// - /// Gets the to map accounts to . + /// Gets the to map accounts to . /// - protected AccountClaimsPrincipalFactory UserFactory { get; } + protected AccountClaimsPrincipalFactory AccountClaimsPrincipalFactory { get; } /// /// Gets the options for the underlying JavaScript library handling the authentication operations. @@ -58,16 +58,16 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication /// The to use for performing JavaScript interop operations. /// The options to be passed down to the underlying JavaScript library handling the authentication operations. /// The used to generate URLs. - /// The used to generate the for the user. + /// The used to generate the for the user. public RemoteAuthenticationService( IJSRuntime jsRuntime, IOptions> options, NavigationManager navigation, - AccountClaimsPrincipalFactory userFactory) + AccountClaimsPrincipalFactory accountClaimsPrincipalFactory) { JsRuntime = jsRuntime; Navigation = navigation; - UserFactory = userFactory; + AccountClaimsPrincipalFactory = accountClaimsPrincipalFactory; Options = options.Value; } @@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication result.RedirectUrl = redirectUrl.ToString(); } - return new AccessTokenResult(parsedStatus, result.Token, Navigation, result.RedirectUrl); + return new AccessTokenResult(parsedStatus, result.Token, result.RedirectUrl); } /// @@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication result.RedirectUrl = redirectUrl.ToString(); } - return new AccessTokenResult(parsedStatus, result.Token, Navigation, result.RedirectUrl); + return new AccessTokenResult(parsedStatus, result.Token, result.RedirectUrl); } private Uri GetRedirectUrl(string customReturnUrl) @@ -217,7 +217,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication { await EnsureAuthService(); var account = await JsRuntime.InvokeAsync("AuthenticationService.getUser"); - var user = await UserFactory.CreateUserAsync(account, Options.UserOptions); + var user = await AccountClaimsPrincipalFactory.CreateUserAsync(account, Options.UserOptions); return user; } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs index b7c1af973c..a3c76564d1 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs @@ -38,6 +38,9 @@ namespace Microsoft.Extensions.DependencyInjection return (IRemoteAuthenticationService)sp.GetRequiredService(); }); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddScoped(sp => { return (IAccessTokenProvider)sp.GetRequiredService(); diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/AuthorizationMessageHandlerTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/AuthorizationMessageHandlerTests.cs new file mode 100644 index 0000000000..768d45d8ec --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/AuthorizationMessageHandlerTests.cs @@ -0,0 +1,208 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication +{ + public class AuthorizationMessageHandlerTests + { + [Fact] + public async Task Throws_IfTheListOfAllowedUrlsIsNotConfigured() + { + // Arrange + var expectedMessage = "The 'AuthorizationMessageHandler' is not configured. " + + "Call 'ConfigureHandler' and provide a list of endpoint urls to attach the token to."; + + var tokenProvider = new Mock(); + + var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of()); + // Act & Assert + + var exception = await Assert.ThrowsAsync( + () => new HttpClient(handler).GetAsync("https://www.example.com")); + + Assert.Equal(expectedMessage, exception.Message); + } + + [Fact] + public async Task DoesNotAttachTokenToRequest_IfNotPresentInListOfAllowedUrls() + { + // Arrange + var tokenProvider = new Mock(); + + var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of()); + handler.ConfigureHandler(new[] { "https://localhost:5001" }); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + handler.InnerHandler = new TestMessageHandler(response); + + // Act + _ = await new HttpClient(handler).GetAsync("https://www.example.com"); + + // Assert + tokenProvider.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RequestsTokenWithDefaultScopes_WhenNoTokenIsAvailable() + { + // Arrange + var tokenProvider = new Mock(); + tokenProvider.Setup(tp => tp.RequestAccessToken()) + .Returns(new ValueTask(new AccessTokenResult(AccessTokenResultStatus.Success, + new AccessToken + { + Expires = DateTime.Now.AddHours(1), + GrantedScopes = new string[] { "All" }, + Value = "asdf" + }, + "https://www.example.com"))); + + var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of()); + handler.ConfigureHandler(new[] { "https://localhost:5001" }); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + handler.InnerHandler = new TestMessageHandler(response); + + // Act + _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather"); + + // Assert + Assert.Equal("asdf", response.RequestMessage.Headers.Authorization.Parameter); + } + + [Fact] + public async Task CachesExistingTokenWhenPossible() + { + // Arrange + var tokenProvider = new Mock(); + tokenProvider.Setup(tp => tp.RequestAccessToken()) + .Returns(new ValueTask(new AccessTokenResult(AccessTokenResultStatus.Success, + new AccessToken + { + Expires = DateTime.Now.AddHours(1), + GrantedScopes = new string[] { "All" }, + Value = "asdf" + }, + "https://www.example.com"))); + + var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of()); + handler.ConfigureHandler(new[] { "https://localhost:5001" }); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + handler.InnerHandler = new TestMessageHandler(response); + + // Act + _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather"); + response.RequestMessage = null; + + _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather"); + + // Assert + Assert.Single(tokenProvider.Invocations); + Assert.Equal("asdf", response.RequestMessage.Headers.Authorization.Parameter); + } + + [Fact] + public async Task RequestNewTokenWhenCurrentTokenIsAboutToExpire() + { + // Arrange + var tokenProvider = new Mock(); + tokenProvider.Setup(tp => tp.RequestAccessToken()) + .Returns(new ValueTask(new AccessTokenResult(AccessTokenResultStatus.Success, + new AccessToken + { + Expires = DateTime.Now.AddMinutes(3), + GrantedScopes = new string[] { "All" }, + Value = "asdf" + }, + "https://www.example.com"))); + + var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of()); + handler.ConfigureHandler(new[] { "https://localhost:5001" }); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + handler.InnerHandler = new TestMessageHandler(response); + + // Act + _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather"); + response.RequestMessage = null; + + _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather"); + + // Assert + Assert.Equal(2, tokenProvider.Invocations.Count); + } + + [Fact] + public async Task ThrowsWhenItCanNotProvisionANewToken() + { + // Arrange + var tokenProvider = new Mock(); + tokenProvider.Setup(tp => tp.RequestAccessToken()) + .Returns(new ValueTask(new AccessTokenResult(AccessTokenResultStatus.RequiresRedirect, + null, + "https://www.example.com"))); + + var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of()); + handler.ConfigureHandler(new[] { "https://localhost:5001" }); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + handler.InnerHandler = new TestMessageHandler(response); + + // Act & assert + var exception = await Assert.ThrowsAsync(() => new HttpClient(handler).GetAsync("https://localhost:5001/weather")); + } + + [Fact] + public async Task UsesCustomScopesAndReturnUrlWhenProvided() + { + // Arrange + var tokenProvider = new Mock(); + tokenProvider.Setup(tp => tp.RequestAccessToken(It.IsAny())) + .Returns(new ValueTask(new AccessTokenResult(AccessTokenResultStatus.Success, + new AccessToken + { + Expires = DateTime.Now.AddMinutes(3), + GrantedScopes = new string[] { "All" }, + Value = "asdf" + }, + "https://www.example.com/return"))); + + var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of()); + handler.ConfigureHandler( + new[] { "https://localhost:5001" }, + scopes: new[] { "example.read", "example.write" }, + returnUrl: "https://www.example.com/return"); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + handler.InnerHandler = new TestMessageHandler(response); + + // Act + _ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather"); + + // Assert + Assert.Equal(1, tokenProvider.Invocations.Count); + } + } + + internal class TestMessageHandler : HttpMessageHandler + { + private readonly HttpResponseMessage _response; + + public TestMessageHandler(HttpResponseMessage response) => _response = response; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + _response.RequestMessage = request; + return Task.FromResult(_response); + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs index 5b8334ef6e..eca45e8913 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticationServiceTests.cs @@ -406,7 +406,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication testJsRuntime, options, new TestNavigationManager(), - new TestUserFactory(Mock.Of())); + new TestAccountClaimsPrincipalFactory(Mock.Of())); var account = new CoolRoleAccount { @@ -442,7 +442,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication testJsRuntime, options, new TestNavigationManager(), - new TestUserFactory(Mock.Of())); + new TestAccountClaimsPrincipalFactory(Mock.Of())); var account = new CoolRoleAccount { @@ -481,39 +481,30 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication private static IOptions> CreateOptions(string scopeClaim = null) { - return Options.Create( - new RemoteAuthenticationOptions() - { - AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions - { - LogInPath = "login", - LogInCallbackPath = "a", - LogInFailedPath = "a", - RegisterPath = "a", - ProfilePath = "a", - RemoteRegisterPath = "a", - RemoteProfilePath = "a", - LogOutPath = "a", - LogOutCallbackPath = "a", - LogOutFailedPath = "a", - LogOutSucceededPath = "a", - }, - UserOptions = new RemoteAuthenticationUserOptions - { - AuthenticationType = "a", - ScopeClaim = scopeClaim, - RoleClaim = "coolRole", - NameClaim = "coolName", - }, - ProviderOptions = new OidcProviderOptions - { - Authority = "a", - ClientId = "a", - DefaultScopes = new[] { "openid" }, - RedirectUri = "https://www.example.com/base/custom-login", - PostLogoutRedirectUri = "https://www.example.com/base/custom-logout", - } - }); + var options = new RemoteAuthenticationOptions(); + + options.AuthenticationPaths.LogInPath = "login"; + options.AuthenticationPaths.LogInCallbackPath = "a"; + options.AuthenticationPaths.LogInFailedPath = "a"; + options.AuthenticationPaths.RegisterPath = "a"; + options.AuthenticationPaths.ProfilePath = "a"; + options.AuthenticationPaths.RemoteRegisterPath = "a"; + options.AuthenticationPaths.RemoteProfilePath = "a"; + options.AuthenticationPaths.LogOutPath = "a"; + options.AuthenticationPaths.LogOutCallbackPath = "a"; + options.AuthenticationPaths.LogOutFailedPath = "a"; + options.AuthenticationPaths.LogOutSucceededPath = "a"; + options.UserOptions.AuthenticationType = "a"; + options.UserOptions.ScopeClaim = scopeClaim; + options.UserOptions.RoleClaim = "coolRole"; + options.UserOptions.NameClaim = "coolName"; + options.ProviderOptions.Authority = "a"; + options.ProviderOptions.ClientId = "a"; + options.ProviderOptions.DefaultScopes.Add("openid"); + options.ProviderOptions.RedirectUri = "https://www.example.com/base/custom-login"; + options.ProviderOptions.PostLogoutRedirectUri = "https://www.example.com/base/custom-logout"; + + return Options.Create(options); } private class TestJsRuntime : IJSRuntime @@ -571,9 +562,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication } } - internal class TestUserFactory : AccountClaimsPrincipalFactory + internal class TestAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory { - public TestUserFactory(IAccessTokenProviderAccessor accessor) : base(accessor) + public TestAccountClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor) { } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs index 88fea7c71c..d9996a124d 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs @@ -200,31 +200,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker()); builder.Services.AddApiAuthorization(options => { - options.AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions - { - LogInPath = "a", - LogInCallbackPath = "b", - LogInFailedPath = "c", - RegisterPath = "d", - ProfilePath = "e", - RemoteRegisterPath = "f", - RemoteProfilePath = "g", - LogOutPath = "h", - LogOutCallbackPath = "i", - LogOutFailedPath = "j", - LogOutSucceededPath = "k", - }; - options.UserOptions = new RemoteAuthenticationUserOptions - { - AuthenticationType = "l", - ScopeClaim = "m", - RoleClaim = "n", - NameClaim = "o", - }; - options.ProviderOptions = new ApiAuthorizationProviderOptions - { - ConfigurationEndpoint = "p" - }; + options.AuthenticationPaths.LogInPath = "a"; + options.AuthenticationPaths.LogInCallbackPath = "b"; + options.AuthenticationPaths.LogInFailedPath = "c"; + options.AuthenticationPaths.RegisterPath = "d"; + options.AuthenticationPaths.ProfilePath = "e"; + options.AuthenticationPaths.RemoteRegisterPath = "f"; + options.AuthenticationPaths.RemoteProfilePath = "g"; + options.AuthenticationPaths.LogOutPath = "h"; + options.AuthenticationPaths.LogOutCallbackPath = "i"; + options.AuthenticationPaths.LogOutFailedPath = "j"; + options.AuthenticationPaths.LogOutSucceededPath = "k"; + options.UserOptions.AuthenticationType = "l"; + options.UserOptions.ScopeClaim = "m"; + options.UserOptions.RoleClaim = "n"; + options.UserOptions.NameClaim = "o"; + options.ProviderOptions.ConfigurationEndpoint = "p"; }); var host = builder.Build(); @@ -298,35 +289,26 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker()); builder.Services.AddOidcAuthentication(options => { - options.AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions - { - LogInPath = "a", - LogInCallbackPath = "b", - LogInFailedPath = "c", - RegisterPath = "d", - ProfilePath = "e", - RemoteRegisterPath = "f", - RemoteProfilePath = "g", - LogOutPath = "h", - LogOutCallbackPath = "i", - LogOutFailedPath = "j", - LogOutSucceededPath = "k", - }; - options.UserOptions = new RemoteAuthenticationUserOptions - { - AuthenticationType = "l", - ScopeClaim = "m", - RoleClaim = "n", - NameClaim = "o", - }; - options.ProviderOptions = new OidcProviderOptions - { - Authority = "p", - ClientId = "q", - DefaultScopes = Array.Empty(), - RedirectUri = "https://www.example.com/base/custom-login", - PostLogoutRedirectUri = "https://www.example.com/base/custom-logout", - }; + options.AuthenticationPaths.LogInPath = "a"; + options.AuthenticationPaths.LogInCallbackPath = "b"; + options.AuthenticationPaths.LogInFailedPath = "c"; + options.AuthenticationPaths.RegisterPath = "d"; + options.AuthenticationPaths.ProfilePath = "e"; + options.AuthenticationPaths.RemoteRegisterPath = "f"; + options.AuthenticationPaths.RemoteProfilePath = "g"; + options.AuthenticationPaths.LogOutPath = "h"; + options.AuthenticationPaths.LogOutCallbackPath = "i"; + options.AuthenticationPaths.LogOutFailedPath = "j"; + options.AuthenticationPaths.LogOutSucceededPath = "k"; + options.UserOptions.AuthenticationType = "l"; + options.UserOptions.ScopeClaim = "m"; + options.UserOptions.RoleClaim = "n"; + options.UserOptions.NameClaim = "o"; + options.ProviderOptions.Authority = "p"; + options.ProviderOptions.ClientId = "q"; + options.ProviderOptions.DefaultScopes.Clear(); + options.ProviderOptions.RedirectUri = "https://www.example.com/base/custom-login"; + options.ProviderOptions.PostLogoutRedirectUri = "https://www.example.com/base/custom-logout"; }); var host = builder.Build(); diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index 0a60896f3e..f81607bc90 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -52,13 +52,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting // Private right now because we don't have much reason to expose it. This can be exposed // in the future if we want to give people a choice between CreateDefault and something // less opinionated. - Configuration = new ConfigurationBuilder(); + Configuration = new WebAssemblyHostConfiguration(); RootComponents = new RootComponentMappingCollection(); Services = new ServiceCollection(); Logging = new LoggingBuilder(Services); - Logging.SetMinimumLevel(LogLevel.Warning); - // Retrieve required attributes from JSRuntimeInvoker InitializeNavigationManager(jsRuntimeInvoker); InitializeDefaultServices(); @@ -111,10 +109,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting } /// - /// Gets an that can be used to customize the application's - /// configuration sources. + /// Gets an that can be used to customize the application's + /// configuration sources and read configuration attributes. /// - public IConfigurationBuilder Configuration { get; } + public WebAssemblyHostConfiguration Configuration { get; } /// /// Gets the collection of root component mappings configured for the application. @@ -177,8 +175,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting public WebAssemblyHost Build() { // Intentionally overwrite configuration with the one we're creating. - var configuration = Configuration.Build(); - Services.AddSingleton(configuration); + Services.AddSingleton(Configuration); // A Blazor application always runs in a scope. Since we want to make it possible for the user // to configure services inside *that scope* inside their startup code, we create *both* the @@ -186,7 +183,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting var services = _createServiceProvider(); var scope = services.GetRequiredService().CreateScope(); - return new WebAssemblyHost(services, scope, configuration, RootComponents.ToArray()); + return new WebAssemblyHost(services, scope, Configuration, RootComponents.ToArray()); } internal void InitializeDefaultServices() diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs new file mode 100644 index 0000000000..e72dcfb320 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs @@ -0,0 +1,187 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting +{ + /// + /// WebAssemblyHostConfiguration is a class that implements the interface of an IConfiguration, + /// IConfigurationRoot, and IConfigurationBuilder. It can be used to simulatneously build + /// and read from a configuration object. + /// + public class WebAssemblyHostConfiguration : IConfiguration, IConfigurationRoot, IConfigurationBuilder + { + private readonly List _providers = new List(); + private readonly List _sources = new List(); + + private readonly List _changeTokenRegistrations = new List(); + private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); + + /// + /// Gets the sources used to obtain configuration values. + /// + IList IConfigurationBuilder.Sources => new ReadOnlyCollection(_sources.ToList()); + + /// + /// Gets the providers used to obtain configuration values. + /// + IEnumerable IConfigurationRoot.Providers => new ReadOnlyCollection(_providers.ToList()); + + /// + /// Gets a key/value collection that can be used to share data between the + /// and the registered instances. + /// + // In this implementation, this largely exists as a way to satisfy the + // requirements of the IConfigurationBuilder and is not populated by + // the WebAssemblyHostConfiguration with any meaningful info. + IDictionary IConfigurationBuilder.Properties { get; } = new Dictionary(); + + /// + public string this[string key] + { + get + { + // Iterate through the providers in reverse to extract + // the value from the most recently inserted provider. + for (var i = _providers.Count - 1; i >= 0; i--) + { + var provider = _providers[i]; + + if (provider.TryGet(key, out var value)) + { + return value; + } + } + + return null; + } + set + { + if (_providers.Count == 0) + { + throw new InvalidOperationException("Can only set property if at least one provider has been inserted."); + } + + foreach (var provider in _providers) + { + provider.Set(key, value); + } + + } + } + + /// + /// Gets a configuration sub-section with the specified key. + /// + /// The key of the configuration section. + /// The . + /// + /// This method will never return null. If no matching sub-section is found with the specified key, + /// an empty will be returned. + /// + public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); + + /// + /// Gets the immediate descendant configuration sub-sections. + /// + /// The configuration sub-sections. + IEnumerable IConfiguration.GetChildren() + { + return _providers + .SelectMany(s => s.GetChildKeys(Enumerable.Empty(), null)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(key => this.GetSection(key)) + .ToList(); + } + + /// + /// Returns a that can be used to observe when this configuration is reloaded. + /// + /// The . + public IChangeToken GetReloadToken() => _changeToken; + + /// + /// Force the configuration values to be reloaded from the underlying sources. + /// + public void Reload() + { + foreach (var provider in _providers) + { + provider.Load(); + } + RaiseChanged(); + } + + private void RaiseChanged() + { + var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken()); + previousToken.OnReload(); + } + + /// + /// Adds a new configuration source, retrieves the provider for the source, and + /// adds a change listener that triggers a reload of the provider whenever a change + /// is detected. + /// + /// The configuration source to add. + /// The same . + public IConfigurationBuilder Add(IConfigurationSource source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + // Ads this source and its associated provider to the source + // and provider references in this class. We make sure to load + // the data from the provider so that values are properly initialized. + _sources.Add(source); + var provider = source.Build(this); + provider.Load(); + + // Add a handler that will detect when the the configuration + // provider has reloaded data. This will invoke the RaiseChanged + // method which maps changes in individual providers to the change + // token on the WebAssemblyHostConfiguration object. + _changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged())); + + // We keep a list of providers in this class so that we can map + // set and get methods on this class to the set and get methods + // on the individual configuration providers. + _providers.Add(provider); + return this; + } + + /// + /// Builds an with keys and values from the set of registered providers. + /// + /// An with keys and values from the registered providers. + public IConfigurationRoot Build() + { + return this; + } + + /// + public void Dispose() + { + // dispose change token registrations + foreach (var registration in _changeTokenRegistrations) + { + registration.Dispose(); + } + + // dispose providers + foreach (var provider in _providers) + { + (provider as IDisposable)?.Dispose(); + } + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostConfigurationTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostConfigurationTest.cs new file mode 100644 index 0000000000..922293fa0a --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostConfigurationTest.cs @@ -0,0 +1,230 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting +{ + public class WebAssemblyHostConfigurationTest + { + [Fact] + public void CanSetAndGetConfigurationValue() + { + // Arrange + var initialData = new Dictionary() { + { "color", "blue" }, + { "type", "car" }, + { "wheels:year", "2008" }, + { "wheels:count", "4" }, + { "wheels:brand", "michelin" }, + { "wheels:brand:type", "rally" }, + }; + var memoryConfig = new MemoryConfigurationSource { InitialData = initialData }; + var configuration = new WebAssemblyHostConfiguration(); + + // Act + configuration.Add(memoryConfig); + configuration["type"] = "car"; + configuration["wheels:count"] = "6"; + + // Assert + Assert.Equal("car", configuration["type"]); + Assert.Equal("blue", configuration["color"]); + Assert.Equal("6", configuration["wheels:count"]); + } + + [Fact] + public void SettingValueUpdatesAllProviders() + { + // Arrange + var initialData = new Dictionary() { { "color", "blue" } }; + var source1 = new MemoryConfigurationSource { InitialData = initialData }; + var source2 = new CustomizedTestConfigurationSource(); + var configuration = new WebAssemblyHostConfiguration(); + + // Act + configuration.Add(source1); + configuration.Add(source2); + configuration["type"] = "car"; + + // Assert + Assert.Equal("car", configuration["type"]); + IConfigurationRoot root = configuration; + Assert.All(root.Providers, provider => + { + provider.TryGet("type", out var value); + Assert.Equal("car", value); + }); + } + + [Fact] + public void CanGetChildren() + { + // Arrange + var initialData = new Dictionary() { { "color", "blue" } }; + var memoryConfig = new MemoryConfigurationSource { InitialData = initialData }; + var configuration = new WebAssemblyHostConfiguration(); + + // Act + configuration.Add(memoryConfig); + IConfiguration readableConfig = configuration; + var children = readableConfig.GetChildren(); + + // Assert + Assert.NotNull(children); + Assert.NotEmpty(children); + } + + [Fact] + public void CanGetSection() + { + // Arrange + var initialData = new Dictionary() { + { "color", "blue" }, + { "type", "car" }, + { "wheels:year", "2008" }, + { "wheels:count", "4" }, + { "wheels:brand", "michelin" }, + { "wheels:brand:type", "rally" }, + }; + var memoryConfig = new MemoryConfigurationSource { InitialData = initialData }; + var configuration = new WebAssemblyHostConfiguration(); + + // Act + configuration.Add(memoryConfig); + var section = configuration.GetSection("wheels").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value); + + // Assert + Assert.Equal(4, section.Count); + Assert.Equal("2008", section["year"]); + Assert.Equal("4", section["count"]); + Assert.Equal("michelin", section["brand"]); + Assert.Equal("rally", section["brand:type"]); + } + + [Fact] + public void CanDisposeProviders() + { + // Arrange + var initialData = new Dictionary() { { "color", "blue" } }; + var memoryConfig = new MemoryConfigurationSource { InitialData = initialData }; + var configuration = new WebAssemblyHostConfiguration(); + + // Act + configuration.Add(memoryConfig); + Assert.Equal("blue", configuration["color"]); + var exception = Record.Exception(() => configuration.Dispose()); + + // Assert + Assert.Null(exception); + } + + [Fact] + public void CanSupportDeeplyNestedConfigs() + { + // Arrange + var dic1 = new Dictionary() + { + {"Mem1", "Value1"}, + {"Mem1:", "NoKeyValue1"}, + {"Mem1:KeyInMem1", "ValueInMem1"}, + {"Mem1:KeyInMem1:Deep1", "ValueDeep1"} + }; + var dic2 = new Dictionary() + { + {"Mem2", "Value2"}, + {"Mem2:", "NoKeyValue2"}, + {"Mem2:KeyInMem2", "ValueInMem2"}, + {"Mem2:KeyInMem2:Deep2", "ValueDeep2"} + }; + var dic3 = new Dictionary() + { + {"Mem3", "Value3"}, + {"Mem3:", "NoKeyValue3"}, + {"Mem3:KeyInMem3", "ValueInMem3"}, + {"Mem3:KeyInMem4", "ValueInMem4"}, + {"Mem3:KeyInMem3:Deep3", "ValueDeep3"}, + {"Mem3:KeyInMem3:Deep4", "ValueDeep4"} + }; + var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 }; + var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 }; + var memConfigSrc3 = new MemoryConfigurationSource { InitialData = dic3 }; + var configuration = new WebAssemblyHostConfiguration(); + + // Act + configuration.Add(memConfigSrc1); + configuration.Add(memConfigSrc2); + configuration.Add(memConfigSrc3); + + // Assert + var dict = configuration.GetSection("Mem1").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value); + Assert.Equal(3, dict.Count); + Assert.Equal("NoKeyValue1", dict[""]); + Assert.Equal("ValueInMem1", dict["KeyInMem1"]); + Assert.Equal("ValueDeep1", dict["KeyInMem1:Deep1"]); + + var dict2 = configuration.GetSection("Mem2").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value); + Assert.Equal(3, dict2.Count); + Assert.Equal("NoKeyValue2", dict2[""]); + Assert.Equal("ValueInMem2", dict2["KeyInMem2"]); + Assert.Equal("ValueDeep2", dict2["KeyInMem2:Deep2"]); + + var dict3 = configuration.GetSection("Mem3").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value); + Assert.Equal(5, dict3.Count); + Assert.Equal("NoKeyValue3", dict3[""]); + Assert.Equal("ValueInMem3", dict3["KeyInMem3"]); + Assert.Equal("ValueInMem4", dict3["KeyInMem4"]); + Assert.Equal("ValueDeep3", dict3["KeyInMem3:Deep3"]); + Assert.Equal("ValueDeep4", dict3["KeyInMem3:Deep4"]); + } + + [Fact] + public void NewConfigurationProviderOverridesOldOneWhenKeyIsDuplicated() + { + // Arrange + var dic1 = new Dictionary() + { + {"Key1:Key2", "ValueInMem1"} + }; + var dic2 = new Dictionary() + { + {"Key1:Key2", "ValueInMem2"} + }; + var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 }; + var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 }; + + var configuration = new WebAssemblyHostConfiguration(); + + // Act + configuration.Add(memConfigSrc1); + configuration.Add(memConfigSrc2); + + // Assert + Assert.Equal("ValueInMem2", configuration["Key1:Key2"]); + } + + private class CustomizedTestConfigurationProvider : ConfigurationProvider + { + public CustomizedTestConfigurationProvider(string key, string value) + => Data.Add(key, value.ToUpper()); + + public override void Set(string key, string value) + { + Data[key] = value; + } + } + + private class CustomizedTestConfigurationSource : IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new CustomizedTestConfigurationProvider("initialKey", "initialValue"); + } + } + } +} diff --git a/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Pages/FetchData.razor b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Pages/FetchData.razor index 6b16470a8c..2967c904de 100644 --- a/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Pages/FetchData.razor +++ b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Pages/FetchData.razor @@ -1,9 +1,8 @@ @page "/fetchdata" @using Wasm.Authentication.Shared +@implements IDisposable @attribute [Authorize] -@inject IAccessTokenProvider AuthenticationService -@inject NavigationManager Navigation - +@inject WeatherForecastClient WeatherForecast

Weather forecast

This component demonstrates fetching data from the server.

@@ -42,15 +41,18 @@ else protected override async Task OnInitializedAsync() { - var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(Navigation.BaseUri); - - var tokenResult = await AuthenticationService.RequestAccessToken(); - - if (tokenResult.TryGetToken(out var token, redirect: true)) + try { - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}"); - forecasts = await httpClient.GetFromJsonAsync("WeatherForecast"); + forecasts = await WeatherForecast.GetForecastAsync(); + } + catch (AccessTokenNotAvailableException exception) + { + exception.Redirect(); } } + + public void Dispose() + { + WeatherForecast.Dispose(); + } } diff --git a/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Program.cs b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Program.cs index 80af27b672..0efb670f10 100644 --- a/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Program.cs +++ b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Program.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -15,7 +17,10 @@ namespace Wasm.Authentication.Client var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddApiAuthorization() - .AddUserFactory(); + .AddAccountClaimsPrincipalFactory(); + + builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) + .AddHttpMessageHandler(); builder.Services.AddSingleton(); diff --git a/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Wasm.Authentication.Client.csproj b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Wasm.Authentication.Client.csproj index 1c8f7b7100..e3881c7b97 100644 --- a/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Wasm.Authentication.Client.csproj +++ b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Wasm.Authentication.Client.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/WeatherForecastClient.cs b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/WeatherForecastClient.cs new file mode 100644 index 0000000000..0f9ff86f07 --- /dev/null +++ b/src/Components/WebAssembly/testassets/Wasm.Authentication.Client/WeatherForecastClient.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using Wasm.Authentication.Shared; + +namespace Wasm.Authentication.Client +{ + public class WeatherForecastClient : IDisposable + { + private readonly HttpClient _client; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + + public WeatherForecastClient(HttpClient client) + { + _client = client; + } + + public Task GetForecastAsync() => + _client.GetFromJsonAsync("WeatherForecast", _cts.Token); + + public void Dispose() + { + _client?.Dispose(); + } + } +} diff --git a/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs b/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs index 23fb14ed10..3dc2a0a11c 100644 --- a/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs +++ b/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs @@ -47,6 +47,22 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests Assert.Equal("Development key3-value", _appElement.FindElement(By.Id("key3")).Text); } + [Fact] + public void WebAssemblyConfiguration_ReloadingWorks() + { + // Verify values from the default 'appsettings.json' are read. + Browser.Equal("Default key1-value", () => _appElement.FindElement(By.Id("key1")).Text); + + // Change the value of key1 using the form in the UI + var input = _appElement.FindElement(By.Id("key1-input")); + input.SendKeys("newValue"); + var submit = _appElement.FindElement(By.Id("trigger-change")); + submit.Click(); + + // Asser that the value of the key has been updated + Browser.Equal("newValue", () => _appElement.FindElement(By.Id("key1")).Text); + } + [Fact] public void WebAssemblyHostingEnvironment_Works() { diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index f6c755d21c..56b7245ae7 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor b/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor index 5f40b677ca..13b95fb037 100644 --- a/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor @@ -8,3 +8,18 @@
@HostEnvironment.Environment
+ +

+ + +

+ +@code { + string newKey1 { get; set; } + + void TriggerChange() + { + Config["key1"] = newKey1; + } +} + diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 6a1a24d7f9..b0ad4fbd44 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Http; using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; using Microsoft.JSInterop; namespace BasicTestApp @@ -45,8 +46,9 @@ namespace BasicTestApp policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false)); }); - builder.Logging.Services.AddSingleton(s => new PrependMessageLoggerProvider("Custom logger", s.GetService())); - builder.Logging.SetMinimumLevel(LogLevel.Information); + builder.Logging.Services.AddSingleton(s => + new PrependMessageLoggerProvider(builder.Configuration["Logging:PrependMessage:Message"], s.GetService())); + builder.Logging.AddConfiguration(builder.Configuration); var host = builder.Build(); ConfigureCulture(host); diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/appsettings.json b/src/Components/test/testassets/BasicTestApp/wwwroot/appsettings.json index 7b07b04091..1e5d60a721 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/appsettings.json +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/appsettings.json @@ -1,4 +1,12 @@ { "key1": "Default key1-value", - "key2": "Default key2-value" + "key2": "Default key2-value", + "Logging": { + "PrependMessage": { + "Message": "Custom logger", + "LogLevel": { + "Default": "Information" + } + } + } } diff --git a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/ComponentsWebAssembly-CSharp.Client.csproj.in b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/ComponentsWebAssembly-CSharp.Client.csproj.in index ba2907b769..4b856535b2 100644 --- a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/ComponentsWebAssembly-CSharp.Client.csproj.in +++ b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/ComponentsWebAssembly-CSharp.Client.csproj.in @@ -17,6 +17,7 @@ +
diff --git a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj index 0619988876..18d8c0fa23 100644 --- a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj +++ b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj @@ -29,6 +29,7 @@ MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion); MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion); MicrosoftEntityFrameworkCoreToolsPackageVersion=$(MicrosoftEntityFrameworkCoreToolsPackageVersion); + MicrosoftExtensionsHttpPackageVersion=$(MicrosoftExtensionsHttpPackageVersion); SystemNetHttpJsonPackageVersion=$(SystemNetHttpJsonPackageVersion) diff --git a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Pages/FetchData.razor b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Pages/FetchData.razor index 926c8c388f..fbf442fff8 100644 --- a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Pages/FetchData.razor +++ b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Pages/FetchData.razor @@ -2,8 +2,6 @@ @*#if (!NoAuth && Hosted) @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject IAccessTokenProvider AuthenticationService -@inject NavigationManager Navigation #endif*@ @*#if (Hosted) @using ComponentsWebAssembly_CSharp.Shared @@ -11,9 +9,7 @@ @*#if (!NoAuth && Hosted) @attribute [Authorize] #endif*@ -@*#if (NoAuth || !Hosted) @inject HttpClient Http -#endif*@

Weather forecast

@@ -55,17 +51,14 @@ else { @*#if (Hosted) @*#if (!NoAuth) - var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(Navigation.BaseUri); - - var tokenResult = await AuthenticationService.RequestAccessToken(); - - if (tokenResult.TryGetToken(out var token, redirect: true)) + try { - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}"); - forecasts = await httpClient.GetFromJsonAsync("WeatherForecast"); + forecasts = await Http.GetFromJsonAsync("WeatherForecast"); + } + catch (AccessTokenNotAvailableException exception) + { + exception.Redirect(); } - #else forecasts = await Http.GetFromJsonAsync("WeatherForecast"); #endif*@ diff --git a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs index d7157f9c53..aed578cd86 100644 --- a/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs +++ b/src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs @@ -3,6 +3,9 @@ using System.Net.Http; using System.Collections.Generic; using System.Threading.Tasks; using System.Text; +#if (!NoAuth && Hosted) +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +#endif using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +22,18 @@ namespace ComponentsWebAssembly_CSharp var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("app"); - builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +#if (!Hosted || NoAuth) + builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +#else + builder.Services.AddHttpClient("ComponentsWebAssembly_CSharp.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) + .AddHttpMessageHandler(); + + // Supply HttpClient instances that include access tokens when making requests to the server project + builder.Services.AddTransient(sp => sp.GetRequiredService().CreateClient("ComponentsWebAssembly_CSharp.ServerAPI")); +#endif +#if(!NoAuth) + +#endif #if (IndividualLocalAuth) #if (Hosted) builder.Services.AddApiAuthorization();