moqui-framework
moqui-framework copied to clipboard
Notification via websocket does not work for Single Page applications like Angular/Ionic
How to reproduce
Create a Single Page (SP) Angular/Ionic application Create a Topic in Moqui Login user using either Login/login service or call ec.user.loginUser(username, password) directly from your own login process Subscribe to the Topic/ALL Topics from the SP application Send a Notification to the Topic
The SP application will never receive the Notification
Issues
I see userId and username are both NULL in:
org.moqui.impl.webapp class MoquiAbstractEndpoint
@Override void onOpen(Session session, EndpointConfig config) {
userId = eci.user.userId
username = eci.user.username
and webSubject.authenticated is false even after user login in:
org.moqui.impl.context class UserFacadeImpl
void initFromHttpRequest(HttpServletRequest request, HttpServletResponse response) {
if (webSubject.authenticated) {
and endpointsByUser and registeredEndPoints are NULL in:
org.moqui.impl.webapp
class NotificationWebSocketListener
void registerEndpoint(NotificationEndpoint endpoint) {
so when onMessage() is called Notification is never sent because registeredEndPoints == null
@Override
void onMessage(NotificationMessage nm) {
if (registeredEndPoints == null) continue
Here are the docs, some summary and code references for more detail:
https://www.moqui.org/m/docs/framework/User+Interface/Notification+and+WebSocket
Nothing in the Moqui server side of things, or the default JS client side of things in MoquiLib.js, is specific to any client side framework. See lines 173 to 235 (currently anyway) for the JS NotificationClient class and usage comments:
https://github.com/moqui/moqui-runtime/blob/master/base-component/webroot/screen/webroot/js/MoquiLib.js#L173
This is used in the plain 'html' render mode by establishing a WebSocket connection for every page load (far from ideal!) and in the 'vuet' render mode by the WebrootVue Vue JS component which is more similar to what you would be doing with Angular (ie with SPA shell that does not reload per screen/page). Here are some references, though this file changes now and then so best to search for 'NotificationClient':
https://github.com/moqui/moqui-runtime/blob/master/base-component/webroot/screen/webroot/js/WebrootVue.js#L1446
https://github.com/moqui/moqui-runtime/blob/master/base-component/webroot/screen/webroot/js/WebrootVue.js#L1461
Try delegating the user authentication to Moqui instead of manually calling ec.user.loginUser(username, password)
in your service. This can be done by sending the Authorization
header in the API request.
Example:
loginUser (username, password) {
let authorization = 'Basic ' + btoa(username + ':' + password)
return MyAxiosClient().post('/my/login/api/path', {}, {
headers: {
'Authorization': authorization
}
})
}
I do exactly that:
loginUser (username, password)
response:
{ "screenPathList" : [ "vapps" ], "currentParameters" : { "_requestBodyText" : "{ "username": "username", "password": "password"}", "username" : "username", "password" : "password", "org.eclipse.jetty.server.newSessionId" : "node01apgzs0koprmv1bcnjhkoxlcbq1", "moqui.session.token.created" : "true", "moqui.request.authenticated" : "true", "moquiRequestStartTime" : 1589973219649 }, "screenUrl" : "/vapps", "screenParameters" : { } }
Then I subscribe to a topic already created:
WebSocketService.ws = new WebSocket("ws://domain:port/notws");
and on open call
onOpen(msg) { WebSocketService.ws.send("subscribe:userTopic");
But the message is never sent.
Thanks so much for your time.
Stephen
On Mon, May 25, 2020 at 4:23 AM Ayman Abi Abdallah [email protected] wrote:
Try delegating the user authentication to Moqui instead of manually calling ec.user.loginUser(username, password) in your service. This can be done by sending the Authorization header in the API request.
Example:
loginUser (username, password) { let authorization = 'Basic ' + btoa(username + ':' + password) return MyAxiosClient().post('/my/login/api/path', {}, { headers: { 'Authorization': authorization } }) }
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/moqui/moqui-framework/issues/416#issuecomment-633447132, or unsubscribe https://github.com/notifications/unsubscribe-auth/AD2DVLNJUF4PYMFTUD2QNATRTITIPANCNFSM4NG7WCJQ .
Did you request the notification permission in your SP? Also, do you have a onMessage
listener configured in the SP to display the notification?
I'm trying to understand if your problem is with Moqui not sending the notification or the SP not displaying it.
Did you request the notification permission in your SP?
I am not sure about this and how you do that
do you have a onMessage listener configured in the SP to display the notification?
yes WebSocketService.connectState.subscribe(state => { switch (state) { case 'connect': WebSocketService.ws = new WebSocket("ws://domain:port/ notws");
WebSocketService.ws.onopen = this.onOpen;
WebSocketService.ws.onmessage = this.onMessage;
WebSocketService.ws.onclose = this.onClose;
WebSocketService.ws.onerror = this.onError;
break;
case 'connecting':
break;
case 'connected':
break;
}
});
onMessage(msg: MessageEvent) { console.log("WebSocketService::onMessage() message: " + JSON.stringify(msg)); // forward message to listeners WebSocketService.topic.next(msg.data); }
Thank you so much for your time
Stephen
On Mon, May 25, 2020 at 9:15 AM Ayman Abi Abdallah [email protected] wrote:
Did you request the notification permission in your SP? Also, do you have a onMessage listener configured in the SP to display the notification?
I'm trying to understand if your problem is with Moqui not sending the notification or the SP not displaying it.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/moqui/moqui-framework/issues/416#issuecomment-633566941, or unsubscribe https://github.com/notifications/unsubscribe-auth/AD2DVLLM5W5IGQLB4ACMRULRTJVODANCNFSM4NG7WCJQ .
The issue has to do with the fact that Browsers such as Chrome, which I mostly use, do not allow Javascript access to cookies "for security reasons." But in order to keep websocket connection/session with Moqui alive, you need to send back the JSESSIONID cookie and X-CSRF-Token header. Where/how does this magic happens?
The easiest way, for me, involves changes in Moqui, within the browser and the single page application (Ionic/Angular).
Moqui
In Moqui, modify the web.xml as follows
Browser In the browser, that is, Chrome, run chrome://flags and disable the following: same-site-by-default-cookies (set to Disabled) cookies-without-same-site-must-be-secure (set to Disabled) This will cause Chrome to send the JSESSIONID cookie with your requests
Single Page Application (Ionic/Angular) Your application is responsible for sending back both JSESSIONID cookie and X-CSRF-Token header with each Moqui request but Chrome takes care of the JSESSIONID cookie for you. Thanks to Angular interceptor support you do not have to send the X-CSRF-Token header from everywhere in your code where you have to make a request to Moqui. see https://medium.com/@swapnil.s.pakolu/angular-interceptors-multiple-interceptors-and-6-code-examples-of-interceptors-59e745b684ec and https://github.com/SwapnilPakolu/Interceptors for an excellent tutorial on Angular interceptors.
Here is an example Angular interceptor for sending back the X-CSRF-Token header from an Ionic application.
export class TokenInterceptorService implements HttpInterceptor {
constructor(private auth: AuthenticationService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let session: ISession = this.auth.getSessionToken();
let xCsrfToken = null;
// if we have xCsrfToken saved from a previous response add it to the header
if (null != session.xCsrfToken ) {
xCsrfToken = session.xCsrfToken;
}
let credentials = {
withCredentials: true
};
if ( Utils.stringNotEmpty(xCsrfToken) ) {
let newHeaders = req.headers;
// If we have a xCsrfToken, we append it to our new headers
if (Utils.stringNotEmpty(xCsrfToken)) {
newHeaders = newHeaders.append('X-CSRF-Token', xCsrfToken);
}
credentials['headers'] = newHeaders;
}
let authReq = req.clone(credentials);
return next.handle(authReq).pipe(
map(resp => {
if (resp instanceof HttpResponse) {
let respHdr = resp.headers;
const xCsrfToken = respHdr.get('X-CSRF-Token');
if (Utils.stringNotEmpty(xCsrfToken)) {
// save a copy of the xCsrfToken response header for requests later
this.auth.setXCsrfToken(xCsrfToken);
}
}
return resp;
})
);
}
}
web.xml change should be:
<session-config>
<cookie-config>
<comment>__SAME_SITE_NONE__</comment>
</cookie-config>
</session-config>