UNPKG

@citrineos/util

Version:

The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.

168 lines 8.37 kB
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project // // SPDX-License-Identifier: Apache-2.0 import { AuthorizationStatusEnum, AuthorizationWhitelistEnum, } from '@citrineos/base'; import { Logger } from 'tslog'; import { OidcTokenProvider } from '../authorization/index.js'; export class RealTimeAuthorizer { _locationRepository; _config; _logger; _oidcTokenProvider; constructor(locationRepository, config, logger) { this._locationRepository = locationRepository; this._config = config; this._logger = logger ? logger.getSubLogger({ name: this.constructor.name }) : new Logger({ name: this.constructor.name }); if (config.oidcClient) { this._oidcTokenProvider = new OidcTokenProvider(config.oidcClient, this._logger); } } async authorize(authorization, context, evse, connector) { if (!authorization.realTimeAuthUrl) { this._logger.debug(`No Realtime Auth URL from authorization ${authorization.id}`); return authorization.status; } else if (!authorization.realTimeAuth || authorization.realTimeAuth === AuthorizationWhitelistEnum.Allowed) { this._logger.debug(`Realtime Auth whitelisted for authorization ${authorization.id}`); return authorization.status; } else if (authorization.status !== AuthorizationStatusEnum.Accepted) { this._logger.debug(`Skipping Realtime Auth for authorization ${authorization.id} with status ${authorization.status}`); return authorization.status; } let evseId = undefined; let connectorId = undefined; let result = AuthorizationStatusEnum.Invalid; try { const chargingStation = await this._locationRepository.readChargingStationByStationId(context.tenantId, context.stationId); // Determine evseId and connectorId // Priority: provided evse and connector > provided evse with single connector > station with single evse and single connector if (evse && connector) { evseId = evse.id; connectorId = connector.id; } else if (evse && !connector && evse.connectors?.length === 1) { evseId = evse.id; connectorId = evse.connectors[0].id; } else if (chargingStation && chargingStation.evses && chargingStation.evses.length === 1 && chargingStation.evses[0].connectors?.length === 1) { evseId = chargingStation.evses[0].id; connectorId = chargingStation.evses[0].connectors[0].id; } if (evseId === undefined || connectorId === undefined) { this._logger.error(`Cannot determine evseId and connectorId for Realtime Auth of authorization ${authorization.id}`); return authorization.status; } else if (authorization.realTimeAuthLastAttempt) { const realTimeAuthLastAttempt = authorization.realTimeAuthLastAttempt; // Check if last attempt was at the same station and connector within the timeout period if (context.stationId === realTimeAuthLastAttempt.stationId && connectorId === realTimeAuthLastAttempt.connectorId) { const lastAttempt = new Date(realTimeAuthLastAttempt.timestamp); const timeout = authorization.realTimeAuthTimeout ?? this._config.realTimeAuthDefaultTimeoutSeconds; const now = new Date(); const diffInSeconds = (now.getTime() - lastAttempt.getTime()) / 1000; if (diffInSeconds < timeout) { this._logger.debug(`Skipping Realtime Auth for authorization ${authorization.id} due to timeout (${diffInSeconds}s < ${timeout}s)`); return realTimeAuthLastAttempt.result; } } } // Build locationId safely — EVSE may not have an associated Location (#163) const locationId = chargingStation?.locationId?.toString(); if (!locationId) { this._logger.warn(`Charging station '${context.stationId}' has no associated location. ` + `Proceeding with Realtime Auth for authorization ${authorization.id} without locationId.`); } const payload = { tenantPartnerId: authorization.tenantPartnerId, // Required if authorization has RealTimeAuth idToken: authorization.idToken, idTokenType: authorization.idTokenType, locationId: locationId, stationId: context.stationId, evseId: evseId, connectorId: connectorId, }; this._logger.debug(`Sending Realtime Auth request for authorization ${authorization.id} to url: ${authorization.realTimeAuthUrl}`); const headers = { 'Content-Type': 'application/json', }; if (this._oidcTokenProvider) { try { const token = await this._oidcTokenProvider.getToken(); headers['Authorization'] = `Bearer ${token}`; } catch (error) { this._logger.error('Failed to get OIDC token:', error); return AuthorizationStatusEnum.Invalid; } } const response = await fetch(authorization.realTimeAuthUrl, { method: 'POST', headers, body: JSON.stringify(payload), }); const responseJson = await response.json(); // Validate response structure before accessing nested properties (#164) if (!responseJson || typeof responseJson !== 'object' || !('data' in responseJson) || !responseJson.data || typeof responseJson.data !== 'object' || !('allowed' in responseJson.data)) { this._logger.error(`Malformed Realtime Auth response for authorization ${authorization.id}: ` + `expected { data: { allowed: string } } but received: ${JSON.stringify(responseJson)}`); if (authorization.realTimeAuth === 'AllowedOffline') { result = AuthorizationStatusEnum.Accepted; } } else { const realTimeAuth = responseJson; this._logger.debug(`Real time auth response: ${realTimeAuth.data.allowed}`); switch (realTimeAuth.data.allowed) { case 'ALLOWED': result = AuthorizationStatusEnum.Accepted; break; case 'BLOCKED': result = AuthorizationStatusEnum.Blocked; break; case 'EXPIRED': result = AuthorizationStatusEnum.Expired; break; case 'NO_CREDIT': result = AuthorizationStatusEnum.NoCredit; break; case 'NOT_ALLOWED': result = AuthorizationStatusEnum.NotAtThisLocation; break; default: result = AuthorizationStatusEnum.Unknown; } } } catch (error) { this._logger.error(`Real-Time Auth failed: ${error}`); if (authorization.realTimeAuth === 'AllowedOffline') { result = AuthorizationStatusEnum.Accepted; } } authorization.realTimeAuthLastAttempt = { timestamp: new Date().toISOString(), result, stationId: context.stationId, evseId: evseId, connectorId: connectorId, }; authorization.save().catch((error) => { this._logger.error(`Failed to save realTimeAuthLastAttempt for authorization ${authorization.id}: ${error}`); }); return result; } } //# sourceMappingURL=RealTimeAuthorizer.js.map