moqui-framework icon indicating copy to clipboard operation
moqui-framework copied to clipboard

Notification via websocket does not work for Single Page applications like Angular/Ionic

Open agyepong opened this issue 4 years ago • 7 comments

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
		
		
		
	

agyepong avatar May 21 '20 16:05 agyepong

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

jonesde avatar May 22 '20 19:05 jonesde

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
      }
    })
}

aabiabdallah avatar May 25 '20 08:05 aabiabdallah

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 .

agyepong avatar May 25 '20 11:05 agyepong

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.

aabiabdallah avatar May 25 '20 13:05 aabiabdallah

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 .

agyepong avatar May 25 '20 15:05 agyepong

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 SAME_SITE_NONE Note that this is for development only. In production, you may not want it to be NONE. The other options are: SAME_SITE_LAX and SAME_SITE_STRICT

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;
        })
    );
}

}

agyepong avatar Mar 24 '21 02:03 agyepong

web.xml change should be:

<session-config>
    <cookie-config>
        <comment>__SAME_SITE_NONE__</comment>
    </cookie-config>
</session-config>

agyepong avatar Mar 24 '21 03:03 agyepong