Spartacus Product Configurator - Authorization Code Flow breaks deep links - spartacus-storefront

I am currently working on implementing the authorization code flow with Auth0 for our Spartacus application. The authentication works fine and I can navigate the page from the root. However when I try to access a deep link, e.g. /configure/vc/product/entityKey/key I get redirected to the Homepage. Same behaviour when I refresh the page.
I observed that the pages request gets canceled and instead the Router routes to "/". I don't see any Fail Actions being fired by ngrx.
Debugging the flow, I believe it has to do with the AuthService.checkOAuthParamsInUrl() method, which throws an exception in line
const result = await this.oAuthLibWrapperService.tryLogin(); (https://github.com/SAP/spartacus/blob/develop/projects/core/src/auth/user-auth/facade/auth.service.ts)
The method seems to check for the code inside the url, which is not available in deep links. I assumed it would take the token from the local storage if available.
Am I on the wrong track, or is the only possiblity to adjust the behaviour of the AuthService? If so, how should I go on with it?
Spartacus Version: 3.4.3.
rxjs Version: 6.6.7
[EDIT] My AuthConfig:
provideConfig(<AuthConfig>{
authentication: {
OAuthLibConfig: {
responseType: 'code',
redirectUri: environment.spartacus.auth.redirectUrl,
customQueryParams: {
connection: 'main-tenant-oidc',
audience: 'my-audience'
}
},
baseUrl: 'https://my-auth.auth0.com',
client_id: 'id',
client_secret: 'secret',
loginUrl: '/authorize',
tokenEndpoint: '/oauth/token',
userinfoEndpoint: '/userinfo',
revokeEndpoint: '/oauth/revoke'
}
})
[EDIT]
The AuthGuard confirms that the user is logged in, i.e. the isUserLoggedIn() function returns true.
[EDIT]
Updated to Spartacus 4 following the reference structure, no change of behaviour. I am configuring authentication via
routing: {protected: true}
My Feature Module looks like this, knowing that I definitly dont need all of the imports:
import {NgModule} from '#angular/core';
import {OrderConfirmationModule, ReplenishmentOrderConfirmationModule} from "#spartacus/checkout/components";
import {CheckoutOccModule} from "#spartacus/checkout/occ";
import {
AuthModule,
CartModule,
CartOccModule,
CostCenterOccModule,
ExternalRoutesModule,
ProductModule,
ProductOccModule,
UserOccModule
} from "#spartacus/core";
// TODO:Spartacus - 'ProductVariantsModule' was removed from #spartacus/storefront. Use #spartacus/product/variants feature-library instead. To benefit from lazy loading it by default, consider removing the module import and running the command 'ng add #spartacus/product --features=Product-Variants'.
// TODO:Spartacus - 'UserComponentModule' - Following module imports 'LoginModule', 'LoginFormModule', 'LoginRegisterModule', 'RegisterComponentModule' were removed. Those modules are now part of #spartacus/user.
import {
AddressBookModule,
BannerCarouselModule,
BannerModule,
BreadcrumbModule,
CartComponentModule,
CartPageEventModule,
CategoryNavigationModule,
CmsParagraphModule,
FooterNavigationModule,
HamburgerMenuModule,
LinkModule,
MyCouponsModule,
MyInterestsModule,
NavigationEventModule,
NavigationModule,
NotificationPreferenceModule,
OrderCancellationModule,
OrderDetailsModule,
OrderHistoryModule,
OrderReturnModule,
PaymentMethodsModule,
ProductCarouselModule,
ProductDetailsPageModule,
ProductFacetNavigationModule,
ProductImagesModule,
ProductIntroModule,
ProductListingPageModule,
ProductListModule,
ProductPageEventModule,
ProductReferencesModule,
ProductSummaryModule,
ProductTabsModule,
ReplenishmentOrderDetailsModule,
ReplenishmentOrderHistoryModule,
ReturnRequestDetailModule,
ReturnRequestListModule,
SearchBoxModule,
SiteContextSelectorModule,
StockNotificationModule,
TabParagraphContainerModule,
WishListModule
} from "#spartacus/storefront";
import {
CloseAccountModule,
ForgotPasswordModule,
RegisterComponentModule,
ResetPasswordModule,
UpdateEmailModule,
UpdatePasswordModule,
UpdateProfileModule
} from "#spartacus/user/profile/components";
import {ProductVariantsFeatureModule} from './features/product/product-variants-feature.module';
import {LoginFormModule, LoginModule, LoginRegisterModule} from "#spartacus/user/account/components";
import { UserFeatureModule } from './features/user/user-feature.module';
#NgModule({
declarations: [],
imports: [
// Migrating the StorefrontModule
ProductDetailsPageModule,
ProductListingPageModule,
ExternalRoutesModule.forRoot(),
// Migrating the CmsLibModule
HamburgerMenuModule,
CmsParagraphModule,
LinkModule,
BannerModule,
CategoryNavigationModule,
NavigationModule,
FooterNavigationModule,
BreadcrumbModule,
SearchBoxModule,
SiteContextSelectorModule,
AddressBookModule,
OrderHistoryModule,
OrderCancellationModule,
OrderReturnModule,
ReturnRequestListModule,
ReturnRequestDetailModule,
ProductListModule,
ProductFacetNavigationModule,
ProductTabsModule,
ProductCarouselModule,
ProductReferencesModule,
OrderDetailsModule,
PaymentMethodsModule,
CartComponentModule,
TabParagraphContainerModule,
OrderConfirmationModule,
ProductImagesModule,
ProductSummaryModule,
ProductIntroModule,
BannerCarouselModule,
MyCouponsModule,
WishListModule,
NotificationPreferenceModule,
MyInterestsModule,
StockNotificationModule,
ReplenishmentOrderHistoryModule,
ReplenishmentOrderConfirmationModule,
ReplenishmentOrderDetailsModule,
CloseAccountModule,
UpdateEmailModule,
UpdatePasswordModule,
UpdateProfileModule,
ForgotPasswordModule,
ResetPasswordModule,
// Migrating the StorefrontFoundationModule
AuthModule.forRoot(),
CartModule.forRoot(),
ProductModule.forRoot(),
// Migrating the OccModule
CartOccModule,
CheckoutOccModule,
ProductOccModule,
UserOccModule,
CostCenterOccModule,
// Migrating the EventsModule
CartPageEventModule,
NavigationEventModule,
ProductPageEventModule,
ProductVariantsFeatureModule,
// UserComponentModule Substitution
LoginModule,
LoginFormModule,
LoginRegisterModule,
RegisterComponentModule,
UserFeatureModule
]
})
export class SpartacusFeaturesModule {
}
[EDIT]
Important to notice: Our tokens are not being validated by SAP Commerce Backend. So it may be an issue with the error responses, that differ from the native error responses.
Thanks for any hints :)

The throw from this method is not unusual and is handled with the try-catch. This confirms even that if this throws we don't invoke this.authRedirectService.redirect(); method.
In my opinion the depth of the url should not make any difference to Auth handling. I would look either into other guards that could cause some redirect or into this code specific to this configurator routes.

Related

Custom PageLayoutComponent

To have specific layout for some pages at our project we create few custom PageLayoutComponent's. Some contfiguration example:
{
// #ts-ignore
path: null,
canActivate: [CmsPageGuard],
component: CartPageLayoutComponent,
data: {
cxRoute: 'cart',
cxContext: {
[ORDER_ENTRIES_CONTEXT]: ActiveCartOrderEntriesContextToken,
},
},
},
All work fine with storefront until you will not try to select specific page at smartedit. As result it not use our custom CartPageLayoutComponent, but will use PageLayoutComponent for rendering.
Probably this is because it's not a normal route navigation. Can somebody from spartacus team suggest how this bug can be fixed?
Probably this is because it's not a normal route navigation
I believe your Route should be recognized normally, there is nothing special in adding a custom Angular Route.
So I guess there is something special about the page or URL of Spartacus Storefront that runs in your SmartEdit.
It's hard to tell the reason of your problem without more debugging.
You said your app works as expected when run differently (locally?), but when used in SmartEdit, then there is a problem. Please identify factors that makes the run SmartEdit different from your (local?) run. And try to isolate them. Guesses from top of my head:
production vs dev mode of the build?
exact URL of the cart page?
any difference in configuration between a local version and deployed one to be used in SmartEdit?
I would also add some debugging code to better know which routes configs are available and which one is used for the current route. For debugging purposes please add the following constructor logic in your AppModule:
export class AppModule {
// on every page change, log:
// - active url
// - the Route object that was matched with the active URL
// - all the Route objects provided to Angular Router
constructor(router: Router) {
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
console.log({
activeUrl: router.url,
activeRouteConfig:
router.routerState.snapshot.root.firstChild.routeConfig,
allRoutesConfigs: router.config,
});
}
});
}
}
The pages opened in SmartEdit have the same route of cx-preview (e.g. to open faq page in smartedit, request is https://localhost:4200/electronics-spa/en/USD/cx-preview?cmsTicketId=xxxxx. backend can get page id from cmsTicketId). If you want to change the page layout, you can consider use PageLayoutHandler. Spartacus has some PageLayoutHandlers, e.g.
{
provide: PAGE_LAYOUT_HANDLER,
useExisting: CartPageLayoutHandler,
multi: true,
},

created hook for vuex / nuxtClientInit?

I was wondering whats the best way to do something like nuxtClientInit. I'm trying to load the Auth State as early as possible on the client and save it in my vuex store but not on the server side. It would be great if vuex had something like the created hook for components but that doesn't exist to my knowledge.
How could I achieve this behavior? One way could be calling an action from a layout but is that the best way?
I understand the nuxt team are working on a nuxtClientInit feature but before they release that you could just make your own. To understand the workflow that nuxt undertakes when there is a request you can look at the lifecycle here. This shows that nuxtServerInit is called first then middleware. During this middleware call nuxt.config.js is served and this contains your custom configuration. Part of this is 'plugins' which as the docs say,
This option lets you define JavaScript plugins that should be run
before instantiating the root Vue.js application.
So if you write a plugin to call a store action you can then get and set your local storage from there. So, start with a nuxt-client-init plugin:
//nuxt-client-init.client.js
export default async context => {
await context.store.dispatch('nuxtClientInit', context)
}
then add the plugin to nuxt.config.js:
//nuxt.config.js
plugins: [
'~/plugins/nuxt-client-init.client.js'
],
If you notice the naming convention here, the .client.js part of the plugin tells nuxt this is a client only plugin and is shorthand for '{ src: '~/plugins/nuxt-client-init.js', mode: 'client' }' which since nuxt 2.4 is the way to define the old '{ src: '~/plugins/nuxt-client-init.js', ssr: false }'. Anyway, you now have a call to your store so you can have an action to call from local storage and set a state item.
//store/index.js
export const actions = {
nuxtClientInit({ commit }, { req }) {
const autho = localStorage.getItem('auth._token.local') //or whatever yours is called
commit('SET_AUTHO', autho)
console.log('From nuxtClientInit - '+autho)
}
}
You probably need to restart your app for that to all take effect but you are then getting and using your Auth State without any of that pesky nuxtServerInit business.

Aurelia store connectTo never sets target property

I set the aurelia-store up as per the docs; in the main.ts at the bottom of all the plugins (from the skeleton app with dotnet core) I have as the last plugin defined:
aurelia.use.standardConfiguration()
.plugin(PLATFORM.moduleName('aurelia-store'), { initialState })
Then my app needs to login the user and save their bearer token.
await aurelia.start();
await aurelia.setRoot(PLATFORM.moduleName("modules/login/login.vm"));
In the login class I am trying to use the #connectTo decorator. However it never sets the dependency property. So I am stuck on this simple part at the very start of the app and my work already suggested not to use Aurelia but I said I wanted to for fast POC.
I've copied the docs exactly and still have the issue. Notably, I had to turn off strictNullCheck in the tsconfig to make the doc code parse.
Login.ts
#connectTo({
target: 'state',
selector: {
userToken: (store) => store.state.pipe(pluck('userToken')),
loginRedirected: (store) => store.state.pipe(pluck('loginRedirected'))
}
})
export class Login {
static inject = [Aurelia, Store]
public state: State;
app: Aurelia;
constructor(Aurelia, private store: Store<State>) {
this.app = Aurelia
store.registerAction('ChangeUserToken', this.changeUserToken)
store.registerAction('LoginRedirected', this.loginRedirect)
}
activate() {
... this.state is always undefined.
if (!this.state.loginRedirected) { //error
}
}
}
I expect the this.state property to have a state object populated from the global state store with the initialState values.
e.g.
{ userToken: "", loginRedirected: false }
I just need to set the userToken in login and retrieve it in app.js. This is not possible; what could be missing to make this basic function actually work?
ConnectTo is a helper decorator to avoid manual state subscriptions since the Stream of states is a vanilla rxjs observable. If you take a closer look at the official plugin documentation you will notice that it sets up the subscription in a different lifecycle hook.
That said connectTo cant solve everything and with manual subscription you have the most flexibility.
Dont give up with your quest you just had bad luck of falling into a more complicated scenario of startup timing right at the begin which easy enough might bite you with lots of other Frameworks/Libraries as well. Also make sure to visit the official discourse.aurelia.io forum and post back solutions to SO.

Nuxt - How can I run a code in client-side after server-side-rendering?

I created a plugin injecting a noty (https://ned.im/noty/#/) so I can use it globally, it looks like this:
export default ({ app }, inject) => {
const notify = function (options = {}) {
if (process.client) {
new Noty(options).show();
}
}
app.$notify = notify;
inject('notify', notify);
}
This plugin shows a noty only on the client-side. On the server-side a noty does not appear, cause it can be displayed only in browser.
I have a page with product details and I am receiving data in asyncData method. When the product was not found I would like to show a noty with proper message and redirect user to a product list page. When I change a route in client-side everything works awesome. However on the first page load (eg. I change an url manually in the browser) which happens on the server-side a noty does not appear, only a redirect works.
My question is: how to show a noty in this case? How to create a noty in the browser after SSR or what is the best other solution to my problem?
Is there any way to run some code after client-side is already rendered (after server-side-rendering)?
You could just disable ssr for that plugin.
plugins: [
...,
{ src: '~plugins/yourplugin.js', ssr: false }
]
Okay, I found a module for that: https://github.com/potato4d/nuxt-client-init-module
it's not possible right know (nuxt <= 2.14.0 when i answering)
but you can combine client plugin and client middleware to achieve that
please take a look at this link:
https://github.com/nuxt/nuxt.js/issues/2653#issuecomment-390588837

Nuxt Vuex Store Cookies Issue

Good time of the day!
After a few weeks of development of my project, i've decided to migrate from plain Vue to Nuxt.
Mainly because i need SSR, although i know that Google can execute JS presented on the page and therefore generate appropriate content for their search bot.
Another reason is a general workflow of the project development. I like idea with automatic instantiation of routes, store, etc.
I've faced, however, a pretty strange behavior of the application when it is running in the mode: universal instead of mode: spa. And i don't want to switch to mode: spa since then i lose the SSR i was migrating for in the first place.
I' have an account module in the store - account.js which is responsible for handling any operations related to the account management, such as login/logout, get profile of authenticated user, store the token obtained from the login request as well as the logic for handling the 2FA TOTP procedure.
In my legacy application, i was able to get the token from the cookies by just using the following bit of the code
import Cookies from 'js-cookie';
export const state = {
user: null,
token: Cookies.get('token')
}
And save token after the successful authentication by executing the following mutation:
[types.ACCOUNT_SAVE_TOKEN] (state, { token, remember }) {
state.token = token;
Cookies.set('token', token, {
expires: 365,
httpOnly: true
});
}
But after migration to Nuxt.js, every time im loggin in, the token value in the state is getting populated, but no cookie is set, and after navigating to the other page inside the project (it is pwa, so no reloading, etc) token is reset back to null.
This issue however is gone if application is switched to the mode: spa from mode: universal and everything is working just fine.
I've read many issues on the github as well as done pretty big portion of crawling throught the websites which are trying to solve the same issue, though none of the suggestions are working for me.
I've even installed the cookie-universal-nuxt package from NPM which claims to be working with the SSR, but yet every time I'm trying to access this.$cookies.get('token') in the state, or anywhere else (mutations for example), I'm just getting error about using the method (get/set/remove) on null.
Does anyone know or have an idea on how to overcome this issue, at least if it is possible without going back to the mode: spa?
P.S. Running npm run build|generate yields same files as for the normal vue (without the content, just the structure until the target element is readched) while in mode: spa.
Okay, after around 12 hours trying to wrap my head around this issue, i've decided to go the 'dirty' way and create middleware which is doing, in my opinion, way to much processing on each request.
import CookieParser from 'cookieparser';
export default async function ({ store, req }) {
if (!store.getters['account/check']) {
if (!store.state.account.token) {
if (process.server) {
let requestCookies = CookieParser.parse(req.headers.cookie);
if (requestCookies.hasOwnProperty('token')) {
store.dispatch('account/saveToken', {
token: requestCookies.token,
remember: true
});
}
}
}
if (store.state.account.token) {
if (!store.state.account.user) {
try {
await store.dispatch('account/fetchUser');
} catch (error) { }
}
}
}
return Promise.resolve();
}
Seems like useCookie has been created for this use case