Skip to content

OAuth2 in PWA #337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
NadinePe opened this issue May 29, 2018 · 15 comments
Closed

OAuth2 in PWA #337

NadinePe opened this issue May 29, 2018 · 15 comments

Comments

@NadinePe
Copy link

Using this package in version 3.1.4 (since we are on Angular 5 and higher versions are not working) in a Progressive Web App with Azure, implicitFlow and automaticSilentRefresh, we currently have a problem. When stopping the app ("stop all" on Android app overview) and restarting, we always get errors saying "invalid nonce" (that is reproduceable in the browser by cleaning the sessionStorage). I already tried to use "disableOAuth2StateCheck", then the error event of "invalid_nonce_in_state" was gone, but now we have "Error uncaught (in Promise): Wrong nonce: ..." (only reproduceable in the app).
Is it possible that this is the case because service workers do not support sessionStorage (which is our used storage)? How can we then implement this in a PWA? It does not happen in the browser application, also not when using the browser application as Desktop application. Could also be that it is not a good way to init OAuth in our LoginGuard. How is the proposed workflow here?

@jeroenheijmans
Copy link
Collaborator

I'm no expert at PWA's, but if you've traced the problem to sessionStorage then maybe switching to localStorage is a good option? Add this to your app module:

{ provide: OAuthStorage, useValue: localStorage },

and let us know if that helps?

@NadinePe
Copy link
Author

NadinePe commented May 29, 2018

I already tried and it also didn't help. I even couldn't login with localStorage. But maybe also because of some bug fixes in later versions (if I remember right) which I can't use because of Angular 5. Also, I think with localStorage, it will be the same problem: https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/offline-for-pwa. Do I need a storage which is working with service-workers/offline? And how would I do it?

@jeroenheijmans
Copy link
Collaborator

jeroenheijmans commented May 29, 2018

The OAuthStorage type can easily be implemented in a custom way. It has only three methods you need to provide.

So you could write a storage implementation that works in PWAs and then provide that type in the app module (as in my previous comment) instead. In theory. :D

Here's a naive example:

const myStore: OAuthStorage = {
  getItem(key) {
    const data = localStorage.getItem(key);
    console.warn('get', key, data.substring(0, 25));
    return data; 
  },
  setItem(key, data) {
    console.warn('set', key, data.substring(0, 25));
    return localStorage.setItem(key,data);
  },
  removeItem(key) {
    console.warn('remove', key);
    return localStorage.removeItem(key);
  },
};

This example is just a decorator for localStorage, but you could do your own implementation as you see fit.

@NadinePe
Copy link
Author

NadinePe commented May 30, 2018

I think my problem refers to this issue which has not really been solved yet with a for me feasable solution (although closed): #218.

Now I can specify the problem. We have a LoginGuard on the routes which is triggered also when returning from OAuth login with the redirectUrl by the Angular router. The sessionStorage in the PWA won't be set up yet with the token passed through the url and the validation errors in tryLogin() method (was used in LoginGuard) will be triggered (race condition as pointed out in the linked issue above).

So actually, I would need to wait for the specified storage to be filled with the fresh auth data from the redirectUrl params (which for any reason takes longer in the PWA than in the Desktop app - at compare time, my session storage in the PWA is empty -, although I can imagine this case to happen on the desktop, too, when the token is refreshed in the background by silent refresh) and then do the checks.

I now gave it another try by just checking if the tokens are valid and if not, call initimplicitFlow() in the LoginGuard. I put the tryLogin() call into the AppComponent, because it is needed to fill the sessionStorage. But this variant gives me now endless redirects, but also only in the PWA.
Also catching the token_received event and then navigation doesn't help on the PWA.

Meanwhile, I'm really running out of ideas about how it can be fixed and hope for solutions.

@jeroenheijmans
Copy link
Collaborator

@NadinePe The high level description helps. But to be able to do anything concrete a minimal repro would be needed I think. That's also why linked issue was closed: without a concrete, clear way for others to reliably reproduce the scenario there's not much that can be done, unfortunately.

@NadinePe
Copy link
Author

NadinePe commented Jun 4, 2018

So... this is the code. I think the problem is that the redirectUri may not have a guard on it (race conditions), and the redirectUri should then be the route where the tryLogin() happens and the actual starting page is called afterwards. So I'd assume that in the PWAs, it happens that the RouteGuard is active before the sessionStorage gets filled and thus makes the race condition problem.

AppComponent:

export class AppComponent implements OnInit {
public ngOnInit(): void {
 this.oauthService.configure(oAuthConfig);
    this.oauthService.setStorage(sessionStorage);
    this.oauthService.tokenValidationHandler = new NullValidationHandler();
    this.oauthService.setupAutomaticSilentRefresh();
    this.oauthService.tryLogin().catch((error: Error) => this.toaster.error('tokens in url not matching stored tokens'));
  }
}

LoginGuard:

export class LoginGuard implements CanActivate {

  constructor(private oauthService: OAuthService, private store: Store<AppState>) {
  }

  public canActivate(): boolean {
     if (this.oauthService.hasValidAccessToken() && this.oauthService.hasValidIdToken()) {
      this.store.dispatch(new GetUserDataAction());
      return true;
    } else {
      this.oauthService.initImplicitFlow();
      return false;
    }
  }
}

Routes:

[...
 {path: 'dashboard', component: DashboardComponent, canActivate: [LoginGuard]},
 {path: '**', redirectTo: 'dashboard', canActivate: [LoginGuard]}
]

oAuthConfig:

export const oAuthConfig: AuthConfig = {
  'redirectUri': window.location.origin,
 'oidc': true,
  'requestAccessToken': true,
 'silentRefreshRedirectUri': window.location.origin + '/assets/silent-refresh.html',
...}

@NadinePe
Copy link
Author

NadinePe commented Jun 5, 2018

Progress regarding this issue: An unprotected, reachable redirectUri seems to be necessary to avoid the race conditions. I created an AuthComponent calling the tryLogin() method and directing forward to the startpage. The redirectUri for OAuth is now an unprotected route loading this component. The problems have disappeared 👍 Would be great to have this necessarity in the documentation, this has cost us quite some time to figure it out.

@cni13
Copy link

cni13 commented Jun 13, 2018

@NadinePe Which "unprotected, reachable redirectUri" did you choose? We are having a similar issue.

@NadinePe
Copy link
Author

NadinePe commented Jun 13, 2018

For us, it was necessary to refactor our routes a bit, so in the end, we created unguarded main routes on one level higher (those in RouterModule.forRoot()) like this:

export const mainRoutes: Routes = [
// this route may not be protected with the routeGuard because of OAuth race conditions. We need to be able to access the component's tryLogin method after the redirect directly.
{path: 'auth', component: AuthComponent},
{path: '', loadChildren: './features/layout/layout.module#LayoutModule'},
{path: '**', redirectTo: 'auth'}
];

and the child routes are then our actual app routes protected with a login guard (those in RouterModule.forChild).

@cni13
Copy link

cni13 commented Jun 14, 2018

Thank you for your solution @NadinePe . However the solution did not solve our initial problem.

What we did now was to replace the following statement:

this.oauthService.loadDiscoveryDocumentAndLogin().then(
            (doc) => {
            }
        );

with this one:

 this.oauthService.loadDiscoveryDocumentAndLogin({
            disableOAuth2StateCheck: true
        }).then(
            (doc) => {
            }
        );

Setting this property to true is also advised in the defining class when only using OIDC. Because we do exactly that it should be safe.

See https://github.com/manfredsteyer/angular-oauth2-oidc/blob/5d676101c6118d6fa01bfa05b17fb4a58490eaf7/projects/lib/src/types.ts

@raviverel
Copy link

my issue is that i am redirecting from my auth provider to different tab and the nonce dost exit
on the session storage , if i try to disable the state check if faild down the road again ,
any idea ?

@jeroenheijmans
Copy link
Collaborator

Hi @raviverel, I think it might be good to create a minimal repro of your scenario and post a question on Stack Overflow about it. Not sure if this thread (or GitHub issues) in general are the best for this.

@raviverel
Copy link

@jeroenheijmans but this is related to this package and this issue becuase i am getting invalid_nonce_in_state

@cda963
Copy link

cda963 commented Jan 14, 2020

For me, in the angular routes (the home route), commenting out the following line seems to have fixed the problem:

{
	path: '',
	component: HomeComponent,
	pathMatch: 'full',
	//**data: { state: 'home' }**
}

@jeroenheijmans
Copy link
Collaborator

Thx for sharing!

Hoping things got resolved for OP and others. Open a fresh issue with a clear repro if you land here and need more help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants