Files
api/api/src/unraid-api/cli/sso/validate-token.command.ts
Eli Bosley c1ab3a4746 refactor: implement local-session for internal client auth (#1606)
Remove the confusing API keys that were auto-generated for the CLI &
Connect. Instead, support authentication via a custom `local-session`
header.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Local-session authentication for internal/CLI requests
(x-local-session) with generation, validation, on-disk persistence, and
lifecycle init.
* Internal client gains multi-strategy auth (local session, cookie, or
API key), supports subscriptions/origin, and can be cleared/recreated.

* **Security**
  * Embedded development API keys removed from the repository.

* **Refactor**
* Canonical internal client introduced; consumers migrated from legacy
CLI key services.

* **Tests / Chores**
* Tests, env, and gitignore updated for local-session and
canonical-client changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com>
2025-08-27 15:28:25 -04:00

83 lines
2.7 KiB
TypeScript

import { Inject } from '@nestjs/common';
import type { CanonicalInternalClientService } from '@unraid/shared';
import { CANONICAL_INTERNAL_CLIENT_TOKEN } from '@unraid/shared';
import { CommandRunner, SubCommand } from 'nest-commander';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { VALIDATE_OIDC_SESSION_QUERY } from '@app/unraid-api/cli/queries/validate-oidc-session.query.js';
@SubCommand({
name: 'validate-token',
aliases: ['validate', 'v'],
description: 'Returns JSON: { error: string | null, valid: boolean }',
arguments: '<token>',
})
export class ValidateTokenCommand extends CommandRunner {
constructor(
private readonly logger: LogService,
@Inject(CANONICAL_INTERNAL_CLIENT_TOKEN)
private readonly internalClient: CanonicalInternalClientService
) {
super();
}
private createErrorAndExit = (errorMessage: string) => {
this.logger.always(
JSON.stringify({
error: errorMessage,
valid: false,
})
);
process.exit(1);
};
async run(passedParams: string[]): Promise<void> {
if (passedParams.length !== 1) {
this.createErrorAndExit('Please pass token argument only');
}
const token = passedParams[0];
if (typeof token !== 'string' || token.trim() === '') {
this.createErrorAndExit('Invalid token provided');
}
// Always validate as OIDC token
await this.validateOidcToken(token);
}
private async validateOidcToken(token: string): Promise<void> {
try {
const client = await this.internalClient.getClient({ enableSubscriptions: false });
const { data, errors } = await client.query({
query: VALIDATE_OIDC_SESSION_QUERY,
variables: { token },
});
if (errors?.length) {
const errorMessages = errors.map((e) => e.message).join(', ');
this.createErrorAndExit(`GraphQL errors: ${errorMessages}`);
}
const validation = data?.validateOidcSession;
if (validation?.valid) {
this.logger.always(
JSON.stringify({
error: null,
valid: true,
username: validation.username || 'root',
})
);
process.exit(0);
} else {
this.createErrorAndExit('Invalid OIDC session token');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.createErrorAndExit(`Failed to validate OIDC session: ${errorMessage}`);
}
}
}