kaven-utils
Version:
Utils for Node.js.
214 lines (213 loc) • 7.74 kB
JavaScript
/********************************************************************
* @author: Kaven
* @email: kaven@wuwenkai.com
* @website: http://blog.kaven.xyz
* @file: [Kaven-Utils] /src/net/sso/KavenSSOServer.ts
* @create: 2019-02-21 11:27:41.259
* @modify: 2025-10-14 22:58:04.827
* @version: 6.1.0
* @times: 76
* @lines: 283
* @copyright: Copyright © 2019-2025 Kaven. All Rights Reserved.
* @description: [description]
* @license: [license]
********************************************************************/
import { DecodeByRFC3986, GenerateGuid, ParseQueryParameters } from "kaven-basic";
import { ApiRequestEx } from "../../ApiRequestEx.js";
import { HttpGet } from "../../KavenUtility.Net.js";
import { KavenSSO, SSOAction } from "./KavenSSO.js";
export class KavenSSOServer extends KavenSSO {
LoginPath = "/login";
SSOPath = "/sso";
/**
* Server logged in sessions
*/
LoginSessionSet;
// <token, client>
clientsMap = new Map();
Logger;
constructor(secret, options) {
super(secret, options?.parameterName, options?.tokenName);
if (options?.loginPath) {
this.LoginPath = options.loginPath;
}
if (options?.ssoPath) {
this.SSOPath = options.ssoPath;
}
if (options?.sessionSet) {
this.LoginSessionSet = options.sessionSet;
}
else {
this.LoginSessionSet = new Set();
}
}
GetClients(appID, sessionID) {
const array = [];
for (const item of Array.from(this.clientsMap.values())) {
if (item.appID === appID) {
if (sessionID) {
if (sessionID === item.sessionID) {
array.push(item);
break; // client with same appID and sessionID is unique
}
}
}
}
return array;
}
GetClientsByServerSessionID(serverSessionID) {
const array = [];
for (const item of Array.from(this.clientsMap.values())) {
if (item.serverSessionID === serverSessionID) {
array.push(item);
}
}
return array;
}
GetClient(token) {
return this.clientsMap.get(token);
}
/**
*
* @param originalUrl
* @param userID
* @param loginSessionID If defined, a new client will be created
*/
async Verify(originalUrl, userID, loginSessionID) {
// verify request query
const apiResult = await ApiRequestEx.VerifyByHMAC_SHA1(this.Secret, originalUrl);
if (apiResult !== true) {
this.Logger?.Warn(apiResult);
return undefined;
}
const queryParameters = ParseQueryParameters(originalUrl, DecodeByRFC3986);
const result = {
parameters: queryParameters,
};
try {
const action = queryParameters[this.ParameterName.Action];
if (action) {
result.action = action;
}
else {
return result;
}
const appID = queryParameters[this.ParameterName.AppID];
if (!appID) {
this.Logger?.Warn(`AppID: ${appID}`);
return result;
}
const token = queryParameters[this.ParameterName.Token];
const returnTo = queryParameters[this.ParameterName.ReturnTo];
const sessionID = queryParameters[this.ParameterName.SessionID];
const notifyTo = queryParameters[this.ParameterName.NotifyTo];
if (this.EnableLog) {
this.Logger?.Info(`SSO request verified, appID: ${appID}, returnTo: ${returnTo}, sessionID: ${sessionID}`);
}
let client;
if (token) {
client = this.GetClient(token);
}
if (client === undefined) {
// try to get client by sessionID
if (sessionID !== undefined) {
const clients = this.GetClients(appID, sessionID);
if (clients.length > 0) {
client = clients[0];
client.returnTo = returnTo;
}
}
}
if (client === undefined) {
// new client
if (loginSessionID !== undefined) {
client = {
appID: appID,
returnTo,
sessionID: sessionID,
userID,
serverSessionID: loginSessionID,
notifyTo,
token: GenerateGuid(),
};
this.LoginSessionSet.add(loginSessionID);
if (this.EnableLog) {
this.Logger?.Info(`Add new SSO client: ${client}`);
}
}
}
if (client !== undefined) {
this.clientsMap.set(client.token, client);
client.returnTo = returnTo;
result.client = client;
}
return result;
}
catch (ex) {
this.Logger?.Error(ex);
return result;
}
}
MakeNotifyURL(client, action, data) {
if (client.notifyTo) {
const url = new ApiRequestEx(client.notifyTo)
.AddParameter(this.ParameterName.Token, client.token)
.AddParameter(this.ParameterName.AppID, client.appID)
.AddParameter(this.ParameterName.Action, action)
.AddParameter(this.ParameterName.UserID, client.userID)
.AddParameter(this.ParameterName.Data, data)
.MakeByHMAC_SHA1(this.Secret);
return url;
}
return undefined;
}
async MakeRedirectURL(client) {
if (!client.returnTo) {
return undefined;
}
const url = await new ApiRequestEx(client.returnTo)
.AddParameter(this.ParameterName.Action, SSOAction.Login)
.AddParameter(this.ParameterName.Token, client.token)
.AddParameter(this.ParameterName.AppID, client.appID)
.AddParameter(this.ParameterName.UserID, client.userID)
.MakeByHMAC_SHA1(this.Secret);
return url;
}
async GetRedirectToClientURL(client) {
if (!client) {
return undefined;
}
const url = await this.MakeRedirectURL(client);
return url;
}
DeleteToken(token) {
if (this.clientsMap.delete(token)) {
if (this.EnableLog) {
this.Logger?.Info(`delete token: ${token}`);
}
return true;
}
return false;
}
async Logout(serverSessionID, ignoreToken) {
const clients = this.GetClientsByServerSessionID(serverSessionID);
// delete and notify other clients
for (const client of clients) {
this.DeleteToken(client.token);
if (ignoreToken === client.token) {
continue;
}
const url = await this.MakeNotifyURL(client, SSOAction.NotifyLogout);
if (url) {
HttpGet(url).then(res => {
if (this.EnableLog) {
this.Logger?.Info(`notify client[${client.appID}] logout: ${res.Status} - ${res.StatusText}`);
}
}).catch(ex => {
this.Logger?.Error(ex);
});
}
}
this.LoginSessionSet.delete(serverSessionID);
}
}