From 546b52004c2b0ced6013f46d5c36c2f2971b68e4 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Mon, 13 Apr 2020 13:27:53 +0200 Subject: [PATCH] [Blazor][Wasm] Adds HttpMessageHandler to automatically attach tokens to outgoing requests (#20682) * Adds an authorization handler for integration with HttpClient in different scnearios. * Adds a message handler to streamline calling protected resources on the same base address. --- .../Web.JS/dist/Release/blazor.webassembly.js | 2 +- .../RemoteAuthenticationBuilderExtensions.cs | 2 - .../src/Services/AccessTokenResult.cs | 27 +-- .../Services/AuthorizationMessageHandler.cs | 124 +++++++++++ .../BaseAddressAuthorizationMessageHandler.cs | 25 +++ .../src/Services/ExpiredTokenException.cs | 33 +++ .../Services/RemoteAuthenticationService.cs | 4 +- ...thenticationServiceCollectionExtensions.cs | 3 + .../test/AuthorizationMessageHandlerTests.cs | 208 ++++++++++++++++++ .../Hosting/WebAssemblyHostConfiguration.cs | 3 +- .../Pages/FetchData.razor | 24 +- .../Wasm.Authentication.Client/Program.cs | 5 + .../Wasm.Authentication.Client.csproj | 1 + .../WeatherForecastClient.cs | 30 +++ ...ponentsWebAssembly-CSharp.Client.csproj.in | 1 + ...re.Components.WebAssembly.Templates.csproj | 1 + .../Client/Pages/FetchData.razor | 19 +- .../Client/Program.cs | 16 +- 18 files changed, 470 insertions(+), 58 deletions(-) create mode 100644 src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs create mode 100644 src/Components/WebAssembly/WebAssembly.Authentication/src/Services/BaseAddressAuthorizationMessageHandler.cs create mode 100644 src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs create mode 100644 src/Components/WebAssembly/WebAssembly.Authentication/test/AuthorizationMessageHandlerTests.cs create mode 100644 src/Components/WebAssembly/testassets/Wasm.Authentication.Client/WeatherForecastClient.cs 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] { builder.Services.Replace(ServiceDescriptor.Scoped, TAccountClaimsPrincipalFactory>()); - builder.Services.Replace(ServiceDescriptor.Scoped, TUserFactory>()); return builder; } @@ -52,6 +51,5 @@ namespace Microsoft.Extensions.DependencyInjection public static IRemoteAuthenticationBuilder AddAccountClaimsPrincipalFactory( this IRemoteAuthenticationBuilder builder) where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory => builder.AddAccountClaimsPrincipalFactory(); - where TUserFactory : AccountClaimsPrincipalFactory => builder.AddUserFactory(); } } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs index fc71ba1752..8fe4a707b0 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs @@ -11,20 +11,17 @@ 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; } @@ -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 9f8f6c4acf..30e07e27ba 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs @@ -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) 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/src/Hosting/WebAssemblyHostConfiguration.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs index 07c23e6a6d..e72dcfb320 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs @@ -160,8 +160,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting } /// - /// Builds an with keys and values from the set of providers registered in - /// . + /// Builds an with keys and values from the set of registered providers. /// /// An with keys and values from the registered providers. public IConfigurationRoot Build() 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 da3c29b3a7..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; @@ -17,6 +19,9 @@ namespace Wasm.Authentication.Client builder.Services.AddApiAuthorization() .AddAccountClaimsPrincipalFactory(); + builder.Services.AddHttpClient(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) + .AddHttpMessageHandler(); + builder.Services.AddSingleton(); builder.RootComponents.Add("app"); 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/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();