@@ -48,27 +48,49 @@ export enum AuthenticationResultStatus {
4848
4949export interface AuthenticationResult {
5050 status : AuthenticationResultStatus ;
51- state ?: any ;
51+ state ?: unknown ;
5252 message ?: string ;
5353}
5454
5555export interface AuthorizeService {
56- getUser ( ) : Promise < any > ;
56+ getUser ( ) : Promise < unknown > ;
5757 getAccessToken ( request ?: AccessTokenRequestOptions ) : Promise < AccessTokenResult > ;
58- signIn ( state : any ) : Promise < AuthenticationResult > ;
59- completeSignIn ( state : any ) : Promise < AuthenticationResult > ;
60- signOut ( state : any ) : Promise < AuthenticationResult > ;
58+ signIn ( state : unknown ) : Promise < AuthenticationResult > ;
59+ completeSignIn ( state : unknown ) : Promise < AuthenticationResult > ;
60+ signOut ( state : unknown ) : Promise < AuthenticationResult > ;
6161 completeSignOut ( url : string ) : Promise < AuthenticationResult > ;
6262}
6363
6464class OidcAuthorizeService implements AuthorizeService {
6565 private _userManager : UserManager ;
66-
66+ private _intialSilentSignIn : Promise < void > | undefined ;
6767 constructor ( userManager : UserManager ) {
6868 this . _userManager = userManager ;
6969 }
7070
71+ async trySilentSignIn ( ) {
72+ if ( ! this . _intialSilentSignIn ) {
73+ this . _intialSilentSignIn = ( async ( ) => {
74+ try {
75+ await this . _userManager . signinSilent ( ) ;
76+ } catch ( e ) {
77+ // It is ok to swallow the exception here.
78+ // The user might not be logged in and in that case it
79+ // is expected for signinSilent to fail and throw
80+ }
81+ } ) ( ) ;
82+ }
83+
84+ return this . _intialSilentSignIn ;
85+ }
86+
7187 async getUser ( ) {
88+ if ( window . parent === window && ! window . opener && ! window . frameElement && this . _userManager . settings . redirect_uri &&
89+ ! location . href . startsWith ( this . _userManager . settings . redirect_uri ) ) {
90+ // If we are not inside a hidden iframe, try authenticating silently.
91+ await AuthenticationService . instance . trySilentSignIn ( ) ;
92+ }
93+
7294 const user = await this . _userManager . getUser ( ) ;
7395 return user && user . profile ;
7496 }
@@ -120,7 +142,7 @@ class OidcAuthorizeService implements AuthorizeService {
120142 function hasAllScopes ( request : AccessTokenRequestOptions | undefined , currentScopes : string [ ] ) {
121143 const set = new Set ( currentScopes ) ;
122144 if ( request && request . scopes ) {
123- for ( let current of request . scopes ) {
145+ for ( const current of request . scopes ) {
124146 if ( ! set . has ( current ) ) {
125147 return false ;
126148 }
@@ -131,7 +153,7 @@ class OidcAuthorizeService implements AuthorizeService {
131153 }
132154 }
133155
134- async signIn ( state : any ) {
156+ async signIn ( state : unknown ) {
135157 try {
136158 await this . _userManager . clearStaleState ( ) ;
137159 await this . _userManager . signinSilent ( this . createArguments ( ) ) ;
@@ -166,7 +188,7 @@ class OidcAuthorizeService implements AuthorizeService {
166188 }
167189 }
168190
169- async signOut ( state : any ) {
191+ async signOut ( state : unknown ) {
170192 try {
171193 if ( ! ( await this . _userManager . metadataService . getEndSessionEndpoint ( ) ) ) {
172194 await this . _userManager . removeUser ( ) ;
@@ -212,32 +234,32 @@ class OidcAuthorizeService implements AuthorizeService {
212234
213235 private async stateExists ( url : string ) {
214236 const stateParam = new URLSearchParams ( new URL ( url ) . search ) . get ( 'state' ) ;
215- if ( stateParam ) {
216- return await this . _userManager . settings . stateStore ! . get ( stateParam ) ;
237+ if ( stateParam && this . _userManager . settings . stateStore ) {
238+ return await this . _userManager . settings . stateStore . get ( stateParam ) ;
217239 } else {
218240 return undefined ;
219241 }
220242 }
221243
222244 private async loginRequired ( url : string ) {
223245 const errorParameter = new URLSearchParams ( new URL ( url ) . search ) . get ( 'error' ) ;
224- if ( errorParameter ) {
225- const error = await this . _userManager . settings . stateStore ! . get ( errorParameter ) ;
246+ if ( errorParameter && this . _userManager . settings . stateStore ) {
247+ const error = await this . _userManager . settings . stateStore . get ( errorParameter ) ;
226248 return error === 'login_required' ;
227249 } else {
228250 return false ;
229251 }
230252 }
231253
232- private createArguments ( state ?: any ) {
254+ private createArguments ( state ?: unknown ) {
233255 return { useReplaceToNavigate : true , data : state } ;
234256 }
235257
236258 private error ( message : string ) {
237259 return { status : AuthenticationResultStatus . Failure , errorMessage : message } ;
238260 }
239261
240- private success ( state : any ) {
262+ private success ( state : unknown ) {
241263 return { status : AuthenticationResultStatus . Success , state } ;
242264 }
243265
@@ -253,21 +275,57 @@ class OidcAuthorizeService implements AuthorizeService {
253275export class AuthenticationService {
254276
255277 static _infrastructureKey = 'Microsoft.AspNetCore.Components.WebAssembly.Authentication' ;
256- static _initialized : Promise < void > ;
278+ static _initialized : Promise < void > ;
257279 static instance : OidcAuthorizeService ;
280+ static _pendingOperations : { [ key : string ] : Promise < AuthenticationResult > | undefined } = { }
258281
259- public static async init ( settings : UserManagerSettings & AuthorizeServiceSettings ) {
282+ public static init ( settings : UserManagerSettings & AuthorizeServiceSettings ) {
260283 // Multiple initializations can start concurrently and we want to avoid that.
261284 // In order to do so, we create an initialization promise and the first call to init
262285 // tries to initialize the app and sets up a promise other calls can await on.
263286 if ( ! AuthenticationService . _initialized ) {
264- this . _initialized = ( async ( ) => {
265- const userManager = await this . createUserManager ( settings ) ;
287+ AuthenticationService . _initialized = AuthenticationService . initializeCore ( settings ) ;
288+ }
289+
290+ return AuthenticationService . _initialized ;
291+ }
292+
293+ public static handleCallback ( ) {
294+ return AuthenticationService . initializeCore ( ) ;
295+ }
296+
297+ private static async initializeCore ( settings ?: UserManagerSettings & AuthorizeServiceSettings ) {
298+ const finalSettings = settings || AuthenticationService . resolveCachedSettings ( ) ;
299+ if ( ! settings && finalSettings ) {
300+ const userManager = AuthenticationService . createUserManagerCore ( finalSettings ) ;
301+
302+ if ( window . parent !== window && ! window . opener && ( window . frameElement && userManager . settings . redirect_uri &&
303+ location . href . startsWith ( userManager . settings . redirect_uri ) ) ) {
304+ // If we are inside a hidden iframe, try completing the sign in early.
305+ // This prevents loading the blazor app inside a hidden iframe, which speeds up the authentication operations
306+ // and avoids wasting resources (CPU and memory from bootstrapping the Blazor app)
266307 AuthenticationService . instance = new OidcAuthorizeService ( userManager ) ;
267- } ) ( ) ;
308+
309+ // This makes sure that if the blazor app has time to load inside the hidden iframe,
310+ // it is not able to perform another auth operation until this operation has completed.
311+ AuthenticationService . _initialized = ( async ( ) : Promise < void > => {
312+ await AuthenticationService . instance . completeSignIn ( location . href ) ;
313+ return ;
314+ } ) ( ) ;
315+ }
316+ } else if ( settings ) {
317+ const userManager = await AuthenticationService . createUserManager ( settings ) ;
318+ AuthenticationService . instance = new OidcAuthorizeService ( userManager ) ;
319+ } else {
320+ // HandleCallback gets called unconditionally, so we do nothing for normal paths.
321+ // Cached settings are only used on handling the redirect_uri path and if the settings are not there
322+ // the app will fallback to the default logic for handling the redirect.
268323 }
324+ }
269325
270- await this . _initialized ;
326+ private static resolveCachedSettings ( ) : UserManagerSettings | undefined {
327+ const cachedSettings = window . sessionStorage . getItem ( `${ AuthenticationService . _infrastructureKey } .CachedAuthSettings` ) ;
328+ return cachedSettings ? JSON . parse ( cachedSettings ) : undefined ;
271329 }
272330
273331 public static getUser ( ) {
@@ -278,37 +336,46 @@ export class AuthenticationService {
278336 return AuthenticationService . instance . getAccessToken ( ) ;
279337 }
280338
281- public static signIn ( state : any ) {
339+ public static signIn ( state : unknown ) {
282340 return AuthenticationService . instance . signIn ( state ) ;
283341 }
284342
285- public static completeSignIn ( url : string ) {
286- return AuthenticationService . instance . completeSignIn ( url ) ;
343+ public static async completeSignIn ( url : string ) {
344+ let operation = this . _pendingOperations [ url ] ;
345+ if ( ! operation ) {
346+ operation = AuthenticationService . instance . completeSignIn ( url ) ;
347+ await operation ;
348+ delete this . _pendingOperations [ url ] ;
349+ }
350+
351+ return operation ;
287352 }
288353
289- public static signOut ( state : any ) {
354+ public static signOut ( state : unknown ) {
290355 return AuthenticationService . instance . signOut ( state ) ;
291356 }
292357
293- public static completeSignOut ( url : string ) {
294- return AuthenticationService . instance . completeSignOut ( url ) ;
358+ public static async completeSignOut ( url : string ) {
359+ let operation = this . _pendingOperations [ url ] ;
360+ if ( ! operation ) {
361+ operation = AuthenticationService . instance . completeSignOut ( url ) ;
362+ await operation ;
363+ delete this . _pendingOperations [ url ] ;
364+ }
365+
366+ return operation ;
295367 }
296368
297369 private static async createUserManager ( settings : OidcAuthorizeServiceSettings ) : Promise < UserManager > {
298370 let finalSettings : UserManagerSettings ;
299371 if ( isApiAuthorizationSettings ( settings ) ) {
300- let response = await fetch ( settings . configurationEndpoint ) ;
372+ const response = await fetch ( settings . configurationEndpoint ) ;
301373 if ( ! response . ok ) {
302374 throw new Error ( `Could not load settings from '${ settings . configurationEndpoint } '` ) ;
303375 }
304376
305377 const downloadedSettings = await response . json ( ) ;
306378
307- window . sessionStorage . setItem ( `${ AuthenticationService . _infrastructureKey } .CachedAuthSettings` , JSON . stringify ( settings ) ) ;
308-
309- downloadedSettings . automaticSilentRenew = true ;
310- downloadedSettings . includeIdTokenInSilentRenew = true ;
311-
312379 finalSettings = downloadedSettings ;
313380 } else {
314381 if ( ! settings . scope ) {
@@ -323,18 +390,24 @@ export class AuthenticationService {
323390 finalSettings = settings ;
324391 }
325392
326- const userManager = new UserManager ( finalSettings ) ;
393+ window . sessionStorage . setItem ( `${ AuthenticationService . _infrastructureKey } .CachedAuthSettings` , JSON . stringify ( finalSettings ) ) ;
394+
395+ return AuthenticationService . createUserManagerCore ( finalSettings ) ;
396+ }
327397
398+ private static createUserManagerCore ( finalSettings : UserManagerSettings ) {
399+ const userManager = new UserManager ( finalSettings ) ;
328400 userManager . events . addUserSignedOut ( async ( ) => {
329- await userManager . removeUser ( ) ;
401+ userManager . removeUser ( ) ;
330402 } ) ;
331-
332403 return userManager ;
333404 }
334405}
335406
336407declare global {
337- interface Window { AuthenticationService : AuthenticationService ; }
408+ interface Window { AuthenticationService : AuthenticationService }
338409}
339410
411+ AuthenticationService . handleCallback ( ) ;
412+
340413window . AuthenticationService = AuthenticationService ;
0 commit comments