How to properly configure websocket with springboot and reactjs? - reactjs

I can establish a websocket connection with my springboot server but I can't access the endpoint from #MessageMapping when I'm trying to send a message. Here are my configurations:
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/simulator")
.setAllowedOrigins("http://myiphere:3000")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/endpoint");
registry.setApplicationDestinationPrefixes("/app");
}
And a simple controller :
#RestController
#RequestMapping("/api")
public class MyController {
#MessageMapping("/hello/")
#SendTo("/endpoint/greeting")
public Greeting getCurrentLocation() {
System.out.println("hello here");
return GenericBuilder.of(Greeting::new)
.with(Greeting::setContent, "hello from server")
.build();
}
}
I'm using the socketjs-client library in ReactJS by following this tutorial :
import SockJS from "sockjs-client";
import Stomp from "stompjs";
let stompClient;
const connect = () => {
const socket = new SockJS("http://myiphere:8081/simulator");
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log("Connected " + frame);
stompClient.subscribe("http://myiphere:8081/endpoint/greeting", function (greeting) {
console.log("hi" + JSON.parse(greeting.body).content);
});
});
};
const sendSomething = () => {
stompClient.send("http://myiphere:8081/app/hello/", {});
};
And some buttons with onClick events bound to the methods above. The connection is working, I'm getting "connected" messages in browser console but when I'm trying to click the button with sendSomething() I'm not getting anything in the browser's console nor server's console.

Solved.
The problem was the absolute url path in the send() method.
P.S.: And I've been looking for an answer for this problem on many sites and found out that there is no need to use absolute path for subscribe() url.
P.P.S.: In case that someone else have these problems, look for extra / too. You have to be careful when you're setting the url. The pattern from JS should match the one from SpringBoot.

Related

Google login on .NET backend with React application in frontend

EDIT (Thank #possum)
The code under work properly.
TIMEWASTER ALERT
In the option of .AddGoogle(), you can specify a custom callback. This is the callback used in the authentication mechanism between .AddGoogle and the frontend.
In other words, this callback is "internal" and not make by us.
The callback enpoint must are specify in ConfigureExternalAuthenticationProperties("Google", callback url, endpoint after user signin)
I have some issue to make functional a very simple function! Signin on my backend with google (and some other provider).
Before start, you have all of my apologize, the solution to my issue is probably evident for anyone with a good experience on react. It's my first project with react.
So I've read some articles on how integrate Google signin/signup on a backend with NET core and React in frontend. And easy I've read issues found here ...
The code in controller :
[HttpPost("google-login")]
[AllowAnonymous]
[OpenApiOperation("Login / Signup by an external provider.", "Connection with Google, Microsoft, etc")]
public IActionResult GoogleLogin()
{
var redirectUrl = $"https://localhost:5001/api/authentication/google-login-callback";
var properties = _authenticationService.ConstructHandleForGoogleLogin("Google", redirectUrl);
return Challenge(
properties,
"Google");
}
[HttpGet("google-login-callback")]
[AllowAnonymous]
[OpenApiOperation("Callback of agent of login provider.", "Connection with Google, Microsoft, etc")]
public async Task<ActionResult> GoogleLoginCallback()
{
var authenticateResult = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme);
throw new Exception();
}
The code in authentication service :
public AuthenticationProperties ConstructHandleForGoogleLogin(string provider, string redirectUrl)
{
if (provider.ToLowerInvariant() != "google")
throw new InvalidOperationException("Bad provider requested, this is only for Google");
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
properties.AllowRefresh = true;
return properties;
}
The code for the configuration :
internal static class Startup
{
private static readonly ILogger _logger = Log.ForContext(typeof(Startup));
internal static IServiceCollection AddGoogleAuthentication(this IServiceCollection services, IConfiguration config)
{
_logger.Information("Third partie authentication enabled : {0}", "Google");
services.AddOptions<AuthenticationGoogleSettings>()
.BindConfiguration($"SecuritySettings:{nameof(AuthenticationGoogleSettings)}")
.ValidateDataAnnotations()
.ValidateOnStart();
services
.AddAuthorization()
.AddAuthentication(authentication =>
{
authentication.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
authentication.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(googleOptions =>
{
googleOptions.ClientId = config["SecuritySettings:AuthenticationGoogleSettings:ClientId"];
googleOptions.ClientSecret = config["SecuritySettings:AuthenticationGoogleSettings:ClientSecret"];
//googleOptions.CallbackPath = "/api/authentication/google-login-callback"; <- TIMEWAST ALERT
googleOptions.AuthorizationEndpoint += "?prompt=consent";
googleOptions.AccessType = "offline";
googleOptions.Scope.Add("profile");
googleOptions.SignInScheme = IdentityConstants.ExternalScheme;
});
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Strict;
});
return services;
}
}
The code in Frontend :
<form method='POST' action={`https://localhost:5001/api/authentication/google-login`} >
<IconButton
type='submit'
name='provider'
value='Google'>
<Iconify icon="eva:google-fill" color="#DF3E30" />
</IconButton>
</form>

.net core 3.1 Google SSO Callback url not hit

I followed a link to achieve google SSO github.com/aspnet/Security/issues/1370. But even after successful login it is taking me to redirect uri mentioned in authentication property. It is not taking to the callback url. Could someone help on this? Our application is a .net core 3.1 with IdentityServer4.
Am expecting signinoauth2 API to be hit after google login, but thats not happening.
I could see a network call from browser with below format and getting correlation error.
https://localhost:44368/signinoauth2?state=&code=&scope=***&prompt=none
Exception: Correlation failed.
Show raw exception details
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Soulbook.Api.Startup+<>c+<b__5_1>d.MoveNext() in Startup.cs
await next.Invoke();
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
PFB my code for reference,
[HttpGet]
[Authorize(AuthenticationSchemes = GoogleDefaults.AuthenticationScheme)]
[Route("/Feed")]
public ActionResult Feed()
{
return Ok();
}
[HttpGet]
[Route("/signin")]
public ActionResult SignIn()
{
var authProperties = new AuthenticationProperties
{
RedirectUri = "/"
};
return new ChallengeResult(GoogleDefaults.AuthenticationScheme, authProperties);
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async([FromForm]object data)
{
return Ok();
}
Startup.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(o => {
o.LoginPath = "/signin";
o.LogoutPath = "/signout";
o.ExpireTimeSpan = TimeSpan.FromDays(7);
})
.AddGoogle(o => {
o.ClientId = "***";
o.ClientSecret = "**";
o.SaveTokens = true;
o.CallbackPath = "/signinoauth2";
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(GoogleDefaults.AuthenticationScheme)
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).AddNewtonsoftJson();
EDIT: Having signinoauth2 in any one of the below formats also doesnt help.
[HttpGet]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
I assume that you want to get Google user information in your enpoint?
Then what you have to do is configure the external authentication properties. And thanks to this you are going to be able to get the user on your redirect endpoint.
[HttpGet("login/google/")]
[AllowAnonymous]
public async Task<IActionResult> LoginGoogle()
{
var properties = _signInManager.ConfigureExternalAuthenticationProperties(GoogleDefaults.AuthenticationScheme, "/api/identity/google-redirect");
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
What you configured on startup is a callback route which gets handled by Middleware and never hits the endpoint in your controller. What you want to achive is get user on redirect route like this
[HttpGet("google-redirect")]
[AllowAnonymous]
public async Task<IActionResult> CallbackGoogle()
{
var info = await _signInManager.GetExternalLoginInfoAsync();
return Ok();
}
It sounds like you aren't actually being properly authenticated, if you were the app would redirect to the landing page whose controller I assume has an [Authorize] attribute. Could you have possibly forgotten to add yourself as a user in the db that your identity server is referencing?

SignalR + WebAPI + AngularJS + HubContext, Client's still don't receive messages

For the last few days I have been trying to implement SignalR into my AngularJS/WebAPI application.
I have been able to successfully send/receive messages from client to client, however when I push messages purely from the Server, none of the clients receive any messages.
I have seen many people having the same problem, the answer always seems to be using GlobalHost.ConnectionManager.GetHubContext which I have been implementing, without error, however the clients still don't receive any of the messages.
I thought that perhaps it's because WebAPI calls are asynchronous and and therefore takes place on a different thread, but I can't be sure. Please could someone have a look and tell me what I'm doing wrong.
This is my Hub Class:
public class ChatHub : Hub
{
public void Send(string name, string message)
{
// Call the broadcastMessage
Clients.All.broadcastMessage(name, message);
}
public void RunMe()
{
System.Diagnostics.Debug.WriteLine("Client Started");
}
public static void Notify(string name, string message)
{
var hubContext = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
hubContext.Clients.All.broadcastMessage(name, message);
}
}
This is the Angular Controller in Javascript:
$scope.chat = $.connection.chatHub;
$scope.chat.client.broadcastMessage = function (name, message) {
$scope.$apply(function () {
var scope = angular.element($('#discussion')).scope();
scope.chatMessage = message;
alert(message);
});
};
$.connection.hub.start()
.done(function ()
{
console.log('Now connected, connection ID=' + $.connection.hub.id);
$scope.chat.server.runMe();
})
.fail(function(){ console.log('Could not Connect!'); });
$('#sendmessage').click(function () {
$scope.chat.server.send("SERVER", $('#inputMessage').val());
});
This is the Controller that is trying to notify the clients from the server
public class UserController : ApiController
{
#region METHODS
[ActionName("Create")]
[HttpPost]
public HttpResponseMessage Create(JObject parameters)
{
//DYNAMIC DATA
dynamic data = parameters;
//CHECK IF CALL FAILED
if (data == null)
return Request.CreateResponse(HttpStatusCode.InternalServerError, "Request is null");
//PERFORM REQUEST
using (var svc = new UserService())
{
//SET Parameters
String Username = data.Username;
String Password = data.Password;
//NOTIFY USERS
ChatHub.Notify("SERVER", "SERVER MESSAGE");
//CREATE Response
var response = svc.Create(Username, Password);
//RESPOND
return Request.CreateResponse(HttpStatusCode.OK, response);
}
}
}
So just to reiterate, when the "sendmessage" button is clicked on my UI, it sends a message to the server which is then received again by the clients, this works 100%,
However when I call the static Notify method from the Controller None of the clients receive any messages.
Calling the function does not return any errors.
Please could someone help me!
<!--Reference the SignalR library. -->
<script src="Scripts/jquery.signalR-2.2.0.min.js"></script>
Check your jQuery.signalR version.
If you are using dependency injection, the example at ASP.NET is wrong, you have to set your GlobalHost.DependendcyResolver in the the Global.asax file. not in the startup class.

Error 404 when calling Google Cloud Endpoint API from Google Apps Script

I am trying to call a Google Cloud Endpoint API (developed on App Engine) via Google Apps Script. The endpoint is up and running, honestly I don't know which URL I should use but through Google Chrome Web Tools it looks like the URL is something like:
https://myapp.appspot.com/_ah/api/myendpointapi/v1/myEndPointMethod/
Along with API parameters directly included in the URL, separeted by slashes:
https://myapp.appspot.com/_ah/api/myendpointapi/v1/myEndPointMethod/param1value/param2value/...
Now, in order to call that API from Google App Script I am using the following code snippet:
function myFunction() {
var params =
{
"param1" : "param1value",
"param2" : "param2value",
};
var result = UrlFetchApp.fetch('https://myapp.appspot.com/_ah/api/myendpointapi/v1/myEndPointMethod/', params);
DocumentApp.getUi().alert(result);
}
However I always get a 404 error. If I have to be honest I don't even know if UrlFetchApp is the correct way of calling the API. I noticed this thread on StackOverflow but no one answered. What's the correct URL to use? Many thanks.
EDIT: Now I am trying with an API method which does not require any parameter. I found a way to call a specific URL (using method='get' as suggested by the answer below) but now I get a 401 error because it says I am not logged in. I believe I need to use some kind of OAuth parameter now. Any idea? I tryed using OAuthConfig but no luck with that as well :( From App Engine logs I can see the following error:
com.google.api.server.spi.auth.GoogleIdTokenUtils verifyToken: verifyToken: null
com.google.api.server.spi.auth.AppEngineAuthUtils getIdTokenEmail:
getCurrentUser: idToken=null
function myFunction() {
var result = UrlFetchApp.fetch('myurl', googleOAuth_());
result = result.getContentText();
}
function googleOAuth_() {
var SCOPE = 'https://www.googleapis.com/auth/drive';
var NAME = 'myAPIName';
var oAuthConfig = UrlFetchApp.addOAuthService(NAME);
oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope='+SCOPE);
oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
oAuthConfig.setConsumerKey('anonymous');
oAuthConfig.setConsumerSecret('anonymous');
return {oAuthServiceName:NAME, oAuthUseToken:'always'};
}
UrlFetchApp is the only way to call a Google Cloud Endpoints API at the moment. The second parameter to UrlFetchApp.fetch is a special key-value map of advanced options. To pass POST parameters, you need to do the following:
UrlFetchApp.fetch(url, {
method: 'post',
payload: {
"param1" : "param1value",
"param2" : "param2value",
}
});
I was fighting a similar (not the same) problem, when testing feasibility of a GCM backed by EndPoints server. Basically testing if it is possible to get the Google Spreadsheet Appscript to send notification to an Android device. Please bear with me, the following explanation may be a bit convoluted;
Starting with a standard 'Cloud Messaging for Android', backed by the 'App Engine Backend with Google Cloud Messaging', I managed to build a test system that would send messages between Android devices (Github here).
Here is a VERY sparse EndPoints server code that handles register / un-register Android devices, as well as reporting registered devices and sending a message to a list of registered devices.
WARNING: This is not a production quality code, it is stripped of any logging, error handling in order to keep it short.
#Api( name = "gcmEP", version = "v1",
namespace = #ApiNamespace(ownerDomain = "epgcm.example.com", ownerName = "epgcm.example.com", packagePath = "" )
)
public class GcmEP {
#ApiMethod(name = "registToken")
public void registToken(#Named("token") String token) {
if (ofy().load().type(TokenRec.class).filter("token", token).first().now() == null) {
ofy().save().entity(new TokenRec(token)).now();
}
}
#ApiMethod(name = "unregToken")
public void unregToken(#Named("token") String token) {
TokenRec record = ofy().load().type(TokenRec.class).filter("token", token).first().now();
if (record != null) {
ofy().delete().entity(record).now();
}
}
#ApiMethod(name = "listTokens")
public CollectionResponse<TokenRec> listTokens() {
return CollectionResponse.<TokenRec>builder().setItems(ofy().load().type(TokenRec.class).list()).build();
}
#ApiMethod(name = "sendMsg")
public void sendMsg(#Named("message") String message) throws IOException {
if (message != null && message.length() > 0) {
Sender sender = new Sender(System.getProperty("gcm.api.key"));
Message msg = new Message.Builder().addData("message", message).build();
for (TokenRec record : ofy().load().type(TokenRec.class).list()) {
Result result = sender.send(msg, record.getToken(), 4);
if (result.getMessageId() != null) {
// handle CanonicalRegistrationId
} else {
// handle errors, delete record
}
}
}
}
}
Android code for registration and message sending is shown here, even if it is not relevant.
GcmEP mRegSvc;
String mToken;
// register device on EndPoints backend server
private void registerMe() {
new Thread(new RegisterMe(this)).start();
}
private class RegisterMe implements Runnable {
Activity mAct;
public RegisterMe(Activity act) { mAct = act; }
public void run() {
String senderId = null;
if (mAct != null) try {
if (mRegSvc == null) {
mRegSvc = new GcmEP
.Builder(AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), null).setRootUrl(UT.ROOT_URL).build();
}
senderId = getString(R.string.gcm_defaultSenderId);
mToken = InstanceID.getInstance(mAct).getToken(senderId, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
mRegSvc.registToken(mToken).execute();
GcmPubSub.getInstance(mAct).subscribe(mToken, "/topics/global", null); // subscribing to all 'topics' from 'mToken'
} catch (IOException e) { e.printStackTrace(); }
}
}
// send message to EndPoints backend server
new Thread(new Runnable() {
#Override
public void run() {
if (mRegSvc != null) try {
mRegSvc.sendMsg("hello").execute();
} catch (IOException e) { e.printStackTrace(); }
}
}).start();
// receive GCM message
public class GcmListenSvc extends GcmListenerService {
#Override
public void onMessageReceived(String senderId, Bundle data) {
Log.i("_X_", data.getString("message"));
}
}
What is relevant, thought, there is also an APIs Explorer created for the project, that can be used to send messages to your Android device from any browser.
If you use this Explorer, you can see the GET, POST requests for your EndPoints backend server, i.e.
list all registered devices:
GET https://epgcm.appspot.com/_ah/api/gcmEP/v1/tokenrec?fields=items
send a message to all registered devices:
POST https://epgcm.appspot.com/_ah/api/gcmEP/v1/sendMsg/Hello%20World!
Now, you can use this knowledge to send messages to your Android device from an AppScript code as shown:
Version 1: Get list of registered devices and send a GCM message to all of them (or a filtered set).
function sendMsg() {
var msg = 'test from CODE.GS';
var url = 'https://epgcm.appspot.com/_ah/api/gcmEP/v1/tokenrec?fields=items';
var params = { method : 'get'};
var response = UrlFetchApp.fetch(url, params);
var data = JSON.parse(response.getContentText());
var regIds = [];
for (i in data.items)
regIds.push(data.items[i].token);
var payload = JSON.stringify({
'registration_ids' : regIds,
'data' : { 'message' : msg }
});
var params = {
'contentType' : 'application/json',
'headers' : {'Authorization' : 'key=AIza............................'},
'method' : 'post',
'payload' : payload
};
url = 'https://android.googleapis.com/gcm/send';
UrlFetchApp.fetch(url, params);
}
This version relies on code from an old YouTube video, and I don't know if the call to 'android.googleapis.com' is still supported (but it works).
Version 2: Use the EndPoints's 'sendMsg' directly.
function sendMsg() {
var msg = 'test from CODE.GS';
var params = { method : 'post'};
var url = 'https://demoepgcm.appspot.com/_ah/api/gcmEP/v1/sendMsg/' + encodeURIComponent(msg.trim());
UrlFetchApp.fetch(url, params);
}
I have to admit I've never written a line of JavaScript code before, so it may not be up-to-par, but I made it work as a 'proof of concept'.
I would like to get feedback about this problem from people-who-know, since there is so little published info on this specific issue.

Host WebSocket Server in ASP.NET WebAPI on Console Application

I have built an ASP.NET WebAPI which is hosted on a console application. There are some web apis I had created and worked well. Then I tried to implement web socket service on it. The server side code was like below
[RoutePrefix("api/notification")]
public class NotificationController : ApiController
{
[HttpGet]
[Route("")]
public HttpResponseMessage Get()
{
if (HttpContext.Current.IsWebSocketRequest)
{
HttpContext.Current.AcceptWebSocketRequest(new NotificationWebSocketHandler());
return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
}
else
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
}
public class NotificationWebSocketHandler : WebSocketHandler
{
private static WebSocketCollection _clients;
static NotificationWebSocketHandler()
{
_clients = new WebSocketCollection();
Task.Factory.StartNew(() =>
{
while (true)
{
Task.Delay(TimeSpan.FromSeconds(5));
if (_clients.Count > 0)
{
_clients.Broadcast(Guid.NewGuid().ToString());
}
}
});
}
public override void OnOpen()
{
_clients.Add(this);
Console.WriteLine("Web socket client opened, client count = {0}", _clients.Count);
}
public override void OnClose()
{
_clients.Remove(this);
Console.WriteLine("Web socket client closed, client count = {0}", _clients.Count);
}
}
}
But when I opened the client page (which was built on AngularJS) I got the error message said WebSocket connection to 'ws://10.222.115.220:8080/api/notification/' failed: Error during WebSocket handshake: Unexpected response code: 500
My client side code was
app.shared.controllers.controller('DashboardCtrl', function ($q, $scope) {
var ws = new WebSocket("ws://10.222.115.220:8080/api/notification/");
ws.onopen = function () {
console.log('web socket opened');
}
ws.onmessage = function (message) {
$scope.seed = message;
};
$scope.seed = '(empty)';
});
When I attached debug and added a breakpoint at the entry of my Get function, and I found the error 500 was raised without this breakpoint hit. I think this means the error was generated by WebAPI internally.
I'm not sure if anything wrong in my code, or WebSocket feature doesn't support console application host.
you are using the Web API in "Self Host"-mode? (hosted in the console)
There you have the problem that the object HttpContext.Current is "null".
This is a problem of self-hosting. HttpContext.Current is set while using the IIS (web-hosting). This property is not available during self-hosting. I think this could be your problem. (A null-ref-exception is thrown in your web-api-application and returns 500 - internal server error).

Resources