Websocket authentication.

This commit is contained in:
Haiping Chen 2023-11-27 21:19:07 -06:00
parent 8c1b0bc1e0
commit fcc8ba6a3d
16 changed files with 111 additions and 96 deletions

View file

@ -5,7 +5,6 @@ public interface IConversationService
IConversationStateService States { get; }
string ConversationId { get; }
Task<Conversation> NewConversation(Conversation conversation);
Task MarkConnectionReady(string conversationId);
void SetConversationId(string conversationId, List<string> states);
Task<Conversation> GetConversation(string id);
Task<List<Conversation>> GetConversations();

View file

@ -118,15 +118,4 @@ public partial class ConversationService : IConversationService
_state.Load(_conversationId);
states.ForEach(x => _state.SetState(x.Split('=')[0], x.Split('=')[1]));
}
public async Task MarkConnectionReady(string conversationId)
{
var hooks = _services.GetServices<IConversationHook>();
var conv = await GetConversation(conversationId);
foreach (var hook in hooks)
{
// Need to check if user connected with agent is the first time.
await hook.OnUserAgentConnectedInitially(conv);
}
}
}

View file

@ -41,13 +41,6 @@ public class ConversationController : ControllerBase, IApiAdapter
return ConversationViewModel.FromSession(conv);
}
[HttpPatch("/conversation/{conversationId}/ready")]
public async Task ReadyToInteractive([FromRoute] string conversationId)
{
var service = _services.GetRequiredService<IConversationService>();
await service.MarkConnectionReady(conversationId);
}
[HttpGet("/conversations/{agentId}")]
public async Task<IEnumerable<ConversationViewModel>> GetConversations()
{

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using Microsoft.AspNetCore.SignalR;
@ -8,12 +7,15 @@ public class ChatHubConversationHook : ConversationHookBase
{
private readonly IServiceProvider _services;
private readonly IHubContext<SignalRHub> _chatHub;
private readonly IUserIdentity _user;
public ChatHubConversationHook(IServiceProvider services,
IHubContext<SignalRHub> chatHub)
IHubContext<SignalRHub> chatHub,
IUserIdentity user)
{
_services = services;
_chatHub = chatHub;
_user = user;
}
public override async Task OnUserAgentConnectedInitially(Conversation conversation)
@ -33,7 +35,18 @@ public class ChatHubConversationHook : ConversationHookBase
foreach (var message in messages)
{
await Task.Delay(300);
await OnResponseGenerated(new RoleDialogModel(AgentRole.Assistant, message.Text));
await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", new ChatResponseModel()
{
ConversationId = conversation.Id,
Text = message.Text,
Sender = new UserViewModel()
{
FirstName = "AI",
LastName = "Assistant",
Role = AgentRole.Assistant
}
});
}
}
@ -75,7 +88,7 @@ public class ChatHubConversationHook : ConversationHookBase
{
var conv = _services.GetRequiredService<IConversationService>();
await _chatHub.Clients.All.SendAsync("OnMessageReceivedFromAssistant", new ChatResponseModel()
await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", new ChatResponseModel()
{
ConversationId = conv.ConversationId,
MessageId = message.MessageId,

View file

@ -1,25 +1,47 @@
using BotSharp.Abstraction.Conversations.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
namespace BotSharp.Plugin.ChatHub;
[Authorize]
public class SignalRHub : Hub
{
private readonly IServiceProvider _services;
private readonly ILogger _logger;
private readonly IUserIdentity _user;
private readonly IHttpContextAccessor _context;
public SignalRHub(IServiceProvider services,
ILogger<SignalRHub> logger,
IUserIdentity user)
IUserIdentity user,
IHttpContextAccessor context)
{
_services = services;
_logger = logger;
_user = user;
_context = context;
}
public override Task OnConnectedAsync()
public override async Task OnConnectedAsync()
{
Console.WriteLine($"SignalR Hub: {Context.UserIdentifier} connected in {Context.ConnectionId} [{DateTime.Now}]");
return base.OnConnectedAsync();
_logger.LogInformation($"SignalR Hub: {_user.FirstName} {_user.LastName} ({Context.UserIdentifier}) connected in {Context.ConnectionId} [{DateTime.Now}]");
var hooks = _services.GetServices<IConversationHook>();
var convService = _services.GetRequiredService<IConversationService>();
_context.HttpContext.Request.Query.TryGetValue("conversationId", out var conversationId);
var conv = await convService.GetConversation(conversationId);
foreach (var hook in hooks)
{
// Check if user connected with agent is the first time.
if (!conv.Dialogs.Any())
{
await hook.OnUserAgentConnectedInitially(conv);
}
}
await base.OnConnectedAsync();
}
}

View file

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Http;
namespace BotSharp.Plugin.ChatHub;
public class WebSocketsMiddleware
{
private readonly RequestDelegate _next;
public WebSocketsMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var request = httpContext.Request;
// web sockets cannot pass headers so we must take the access token from query param and
// add it to the header before authentication middleware runs
if (request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) &&
request.Query.TryGetValue("access_token", out var accessToken))
{
request.Headers.Add("Authorization", $"Bearer {accessToken}");
}
await _next(httpContext);
}
}

View file

@ -68,6 +68,9 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
app.MapHub<SignalRHub>("/chatHub");
app.UseMiddleware<WebSocketsMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
@ -80,6 +83,4 @@ app.UseBotSharp();
app.UseCors("MyCorsPolicy");
#endif
app.MapHub<SignalRHub>("/chatHub");
app.Run();

View file

@ -1,10 +1,11 @@
import axios from 'axios';
import { getUserStore } from '$lib/helpers/store.js';
/**
* Set axios http headers globally
* @param {string} token
*/
export function setAuthorization(token) {
export function setAuthorization() {
let user = getUserStore();
let headers = axios.defaults.headers;
headers.common['Authorization'] = `Bearer ${token}`;
headers.common['Authorization'] = `Bearer ${user.token}`;
}

View file

@ -13,11 +13,10 @@ export const agentListUrl = `${host}/agents`;
export const agentDetailUrl = `${host}/agent/{id}`;
// conversation
export const conversationInitUrl = `${host}/conversation/{agentId}`
export const conversationReadyUrl = `${host}/conversation/{conversationId}/ready`
export const conversationMessageUrl = `${host}/conversation/{agentId}/{conversationId}`
export const conversationsUrl = `${host}/conversations/{agentId}`
export const dialogsUrl = `${host}/conversation/{conversationId}/dialogs`
export const conversationInitUrl = `${host}/conversation/{agentId}`;
export const conversationMessageUrl = `${host}/conversation/{agentId}/{conversationId}`;
export const conversationsUrl = `${host}/conversations/{agentId}`;
export const dialogsUrl = `${host}/conversation/{conversationId}/dialogs`;
// chathub
export const chatHubUrl = `${host}/chatHub`;

View file

@ -1,5 +1,6 @@
import { userStore, getUserStore } from '$lib/helpers/store.js';
import { tokenUrl, myInfoUrl, usrCreationUrl } from './api-endpoints.js';
import axios from 'axios';
/**
* This callback type is called `requestCallback` and is displayed as a global symbol.
@ -37,31 +38,20 @@ export async function getToken(email, password, onSucceed) {
}
/**
* Set token from exteranl
* @param {string} token
*/
export function setToken(token) {
let user = getUserStore();
userStore.set({ ...user, init: false, loggedIn: true, token: token });
}
/**
* @returns {Promise<import('$typedefs').UserModel>}
*/
export async function myInfo(token) {
const headers = {
Authorization: `Bearer ${token}`,
};
const info = await fetch(myInfoUrl, {
method: 'GET',
headers: headers,
}).then(response => {
if (response.ok) {
return response.json();
} else {
alert(response.statusText);
}
}).then(result => {
let user = getUserStore();
userStore.set({ ...user, init: false, id: result.id });
return result;
})
.catch(error => alert(error.message));
return info;
export async function myInfo() {
const response = await axios.get(myInfoUrl);
return response.data;
}
/**

View file

@ -1,10 +1,9 @@
import {
conversationInitUrl,
conversationMessageUrl,
dialogsUrl,
conversationReadyUrl,
dialogsUrl,
} from './api-endpoints.js';
import { setAuthorization } from '$lib/helpers/http';
import axios from 'axios';
/**
@ -13,27 +12,19 @@ import axios from 'axios';
* @returns {Promise<import('$typedefs').ConversationModel>}
*/
export async function newConversation(agentId) {
setAuthorization();
let url = conversationInitUrl.replace("{agentId}", agentId);
const response = await axios.post(url, {});
return response.data;
}
/**
* Coversation is ready
* @param {string} conversationId
*/
export async function conversationReady(conversationId) {
let url = conversationReadyUrl.replace("{conversationId}", conversationId);
const response = await axios.patch(url, {});
return response.data;
}
/**
* Get dialog history
* @param {string} conversationId
* @returns {Promise<import('$typedefs').ChatResponseModel[]>}
*/
export async function GetDialogs(conversationId) {
setAuthorization();
let url = dialogsUrl.replace("{conversationId}", conversationId);
const response = await axios.get(url);
return response.data;

View file

@ -1,5 +1,6 @@
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import { chatHubUrl } from '$lib/services/api-endpoints.js';
import { getUserStore } from '$lib/helpers/store.js';
// create a writable store to store the connection object
/** @type {HubConnection} */
@ -21,10 +22,12 @@ export const signalr = {
onMessageReceivedFromAssistant: () => {},
// start the connection
async start() {
/** @param {string} conversationId */
async start(conversationId) {
// create a new connection object with the hub URL and some options
let user = getUserStore();
connection = new HubConnectionBuilder()
.withUrl(chatHubUrl) // the hub URL, change it according to your server
.withUrl(chatHubUrl + `?conversationId=${conversationId}&access_token=${user.token}`) // the hub URL, change it according to your server
.withAutomaticReconnect() // enable automatic reconnection
.configureLogging(LogLevel.Information) // configure the logging level
.build();

View file

@ -5,7 +5,7 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { newConversation } from '$lib/services/conversation-service.js';
import { getToken } from '$lib/services/auth-service.js'
import { getToken, setToken } from '$lib/services/auth-service.js'
import { setAuthorization } from '$lib/helpers/http';
const params = $page.params;
@ -17,21 +17,19 @@
let agentId = params.agentId;
onMount(async () => {
let userToken = "";
if(!$page.url.searchParams.has('token')) {
await getToken("guest@gmail.com", "123456", (token) => {
userToken = token;
});
} else {
userToken = $page.url.searchParams.get('token') ?? "unauthorized";
let token = $page.url.searchParams.get('token') ?? "unauthorized";
setToken(token);
}
setAuthorization(userToken);
// new conversation
conversation = await newConversation(agentId);
conversationId = conversation.id;
window.location.href = `/chat/${agentId}/${conversationId}?token=${userToken}`;
window.location.href = `/chat/${agentId}/${conversationId}`;
});
</script>

View file

@ -2,7 +2,6 @@
import Chat from './chat-box.svelte';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { setAuthorization } from '$lib/helpers/http';
import { myInfo } from '$lib/services/auth-service.js';
import { getAgent } from '$lib/services/agent-service.js';
@ -15,10 +14,7 @@
let currentUser;
onMount(async () => {
const token = $page.url.searchParams.get('token') ?? "unauthorized";
setAuthorization(token);
currentUser = await myInfo(token);
currentUser = await myInfo();
// get agent profile
let agentId = params.agentId;

View file

@ -13,9 +13,8 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import Link from 'svelte-link';
import { setAuthorization } from '$lib/helpers/http';
import { signalr } from '$lib/services/signalr-service.js';
import { sendMessageToHub, GetDialogs, conversationReady } from '$lib/services/conversation-service.js';
import { sendMessageToHub, GetDialogs } from '$lib/services/conversation-service.js';
const options = {
scrollbars: {
@ -45,17 +44,12 @@
let dialogs = [];
onMount(async () => {
const token = $page.url.searchParams.get('token') ?? "unauthorized";
setAuthorization(token);
dialogs = await GetDialogs(params.conversationId);
signalr.onMessageReceivedFromClient = onMessageReceivedFromClient;
signalr.onMessageReceivedFromCsr = onMessageReceivedFromCsr;
signalr.onMessageReceivedFromAssistant = onMessageReceivedFromAssistant;
await signalr.start();
await conversationReady(params.conversationId);
await signalr.start(params.conversationId);
const scrollElements = document.querySelectorAll('.scrollbar');
scrollElements.forEach((item) => {

View file

@ -15,7 +15,6 @@
import Headtitle from '$lib/common/HeadTitle.svelte';
import { getToken } from '$lib/services/auth-service.js'
import { goto } from '$app/navigation';
import { setAuthorization } from '$lib/helpers/http';
let username = 'guest@gmail.com';
let password = '';
@ -29,8 +28,7 @@
isOpen = true;
msg = 'Authentication success';
status = 'success';
setAuthorization(token);
goto(`/chat/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a?token=${token}`);
goto(`/chat/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a`);
});
}
</script>