Update token management again

This commit is contained in:
Wolfgang Kulhanek
2025-10-24 13:42:55 +02:00
parent d0d51b02f6
commit 48a71031c6
3 changed files with 40 additions and 25 deletions

View File

@@ -248,7 +248,32 @@ class SonosSoap {
getCredentialsForToken(token: string): SmapiToken | undefined { getCredentialsForToken(token: string): SmapiToken | undefined {
logger.debug("getCredentialsForToken called with: " + token); logger.debug("getCredentialsForToken called with: " + token);
logger.debug("Current tokens: " + JSON.stringify(this.tokenStore.getAll())); logger.debug("Current tokens: " + JSON.stringify(this.tokenStore.getAll()));
return this.tokenStore.get(token);
// First try direct lookup
let smapiToken = this.tokenStore.get(token);
if (smapiToken) {
return smapiToken;
}
// If not found, try to find by service token (for cases where token was refreshed)
// This is a fallback mechanism to handle token refresh scenarios
const allTokens = this.tokenStore.getAll();
for (const [storedToken, storedSmapiToken] of Object.entries(allTokens)) {
try {
const verifyResult = this.smapiAuthTokens.verify(storedSmapiToken);
if (E.isRight(verifyResult)) {
// This token is valid, check if it matches our service token
const serviceToken = verifyResult.right;
// We can't easily extract the service token from the JWT without the key,
// so we'll rely on the direct lookup for now
}
} catch (error) {
// Token is invalid/expired, skip it
continue;
}
}
return undefined;
} }
associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) { associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) {
logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken)); logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken));
@@ -540,8 +565,6 @@ function bindSmapiSoapServiceToExpress(
throw SMAPI_FAULT_LOGIN_UNAUTHORIZED; throw SMAPI_FAULT_LOGIN_UNAUTHORIZED;
}); });
} else if (isExpiredTokenError(authOrFail)) { } else if (isExpiredTokenError(authOrFail)) {
// Don't pass old token here to avoid circular reference issues with Jest/SOAP
// Old expired tokens will be cleaned up by TTL or manual cleanup later
logger.info("Token expired, attempting refresh..."); logger.info("Token expired, attempting refresh...");
throw await pipe( throw await pipe(
musicService.refreshToken(authOrFail.expiredToken), musicService.refreshToken(authOrFail.expiredToken),
@@ -549,7 +572,7 @@ function bindSmapiSoapServiceToExpress(
logger.info("Token refresh successful, issuing new SMAPI token"); logger.info("Token refresh successful, issuing new SMAPI token");
return smapiAuthTokens.issue(it.serviceToken); return smapiAuthTokens.issue(it.serviceToken);
}), }),
TE.tap(swapToken(undefined)), TE.tap(swapToken(credentials?.loginToken?.token)),
TE.map((newToken) => ({ TE.map((newToken) => ({
Fault: { Fault: {
faultcode: "Client.TokenRefreshRequired", faultcode: "Client.TokenRefreshRequired",
@@ -615,12 +638,10 @@ function bindSmapiSoapServiceToExpress(
throw fault.toSmapiFault(); throw fault.toSmapiFault();
}) })
); );
// Don't pass old token here to avoid circular reference issues with Jest/SOAP
// Old expired tokens will be cleaned up by TTL or manual cleanup later
return pipe( return pipe(
musicService.refreshToken(serviceToken), musicService.refreshToken(serviceToken),
TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), TE.map((it) => smapiAuthTokens.issue(it.serviceToken)),
TE.tap(swapToken(undefined)), // ignores the return value, like a tee or peek TE.tap(swapToken(creds?.loginToken?.token)), // Pass the old token to be replaced
TE.map((it) => ({ TE.map((it) => ({
refreshAuthTokenResult: { refreshAuthTokenResult: {
authToken: it.token, authToken: it.token,

View File

@@ -41,13 +41,11 @@ export class InMemorySmapiTokenStore implements SmapiTokenStore {
const smapiToken = this.tokens[tokenKey]; const smapiToken = this.tokens[tokenKey];
if (smapiToken) { if (smapiToken) {
const verifyResult = smapiAuthTokens.verify(smapiToken); const verifyResult = smapiAuthTokens.verify(smapiToken);
// Only delete if token verification fails with InvalidTokenError
// Do NOT delete ExpiredTokenError as those can still be refreshed
if (E.isLeft(verifyResult)) { if (E.isLeft(verifyResult)) {
const error = verifyResult.left; const error = verifyResult.left;
// Only delete invalid tokens, not expired ones (which can be refreshed) // Delete both invalid and expired tokens to prevent accumulation
if (error._tag === 'InvalidTokenError') { if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') {
logger.debug(`Deleting invalid token from in-memory store`); logger.debug(`Deleting ${error._tag} token from in-memory store`);
delete this.tokens[tokenKey]; delete this.tokens[tokenKey];
deletedCount++; deletedCount++;
} }
@@ -56,7 +54,7 @@ export class InMemorySmapiTokenStore implements SmapiTokenStore {
} }
if (deletedCount > 0) { if (deletedCount > 0) {
logger.info(`Cleaned up ${deletedCount} invalid token(s) from in-memory store`); logger.info(`Cleaned up ${deletedCount} token(s) from in-memory store`);
} }
return deletedCount; return deletedCount;
@@ -142,13 +140,11 @@ export class FileSmapiTokenStore implements SmapiTokenStore {
const smapiToken = this.tokens[tokenKey]; const smapiToken = this.tokens[tokenKey];
if (smapiToken) { if (smapiToken) {
const verifyResult = smapiAuthTokens.verify(smapiToken); const verifyResult = smapiAuthTokens.verify(smapiToken);
// Only delete if token verification fails with InvalidTokenError
// Do NOT delete ExpiredTokenError as those can still be refreshed
if (E.isLeft(verifyResult)) { if (E.isLeft(verifyResult)) {
const error = verifyResult.left; const error = verifyResult.left;
// Only delete invalid tokens, not expired ones (which can be refreshed) // Delete both invalid and expired tokens to prevent accumulation
if (error._tag === 'InvalidTokenError') { if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') {
logger.debug(`Deleting invalid token from file store`); logger.debug(`Deleting ${error._tag} token from file store`);
delete this.tokens[tokenKey]; delete this.tokens[tokenKey];
deletedCount++; deletedCount++;
} }
@@ -157,7 +153,7 @@ export class FileSmapiTokenStore implements SmapiTokenStore {
} }
if (deletedCount > 0) { if (deletedCount > 0) {
logger.info(`Cleaned up ${deletedCount} invalid token(s) from file store`); logger.info(`Cleaned up ${deletedCount} token(s) from file store`);
this.saveToFile(); this.saveToFile();
} }

View File

@@ -122,13 +122,11 @@ export class SQLiteSmapiTokenStore implements SmapiTokenStore {
const smapiToken = tokens[tokenKey]; const smapiToken = tokens[tokenKey];
if (smapiToken) { if (smapiToken) {
const verifyResult = smapiAuthTokens.verify(smapiToken); const verifyResult = smapiAuthTokens.verify(smapiToken);
// Only delete if token verification fails with InvalidTokenError
// Do NOT delete ExpiredTokenError as those can still be refreshed
if (E.isLeft(verifyResult)) { if (E.isLeft(verifyResult)) {
const error = verifyResult.left; const error = verifyResult.left;
// Only delete invalid tokens, not expired ones (which can be refreshed) // Delete both invalid and expired tokens to prevent accumulation
if (error._tag === 'InvalidTokenError') { if (error._tag === 'InvalidTokenError' || error._tag === 'ExpiredTokenError') {
logger.debug(`Deleting invalid token from SQLite store`); logger.debug(`Deleting ${error._tag} token from SQLite store`);
this.delete(tokenKey); this.delete(tokenKey);
deletedCount++; deletedCount++;
} }
@@ -137,7 +135,7 @@ export class SQLiteSmapiTokenStore implements SmapiTokenStore {
} }
if (deletedCount > 0) { if (deletedCount > 0) {
logger.info(`Cleaned up ${deletedCount} invalid token(s) from SQLite store`); logger.info(`Cleaned up ${deletedCount} token(s) from SQLite store`);
} }
return deletedCount; return deletedCount;