Skip to content

Commit eabfb95

Browse files
committed
auth subcommand support
1 parent ce4d98a commit eabfb95

File tree

4 files changed

+93
-5
lines changed

4 files changed

+93
-5
lines changed

packages/b2c-cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@
6363
],
6464
"topicSeparator": " ",
6565
"topics": {
66+
"auth": {
67+
"description": "Authentication and token management"
68+
},
6669
"hello": {
6770
"description": "Say hello to the world and others"
6871
},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {ux} from '@oclif/core';
2+
import {OAuthCommand} from '@salesforce/b2c-tooling/cli';
3+
import type {AccessTokenResponse} from '@salesforce/b2c-tooling/auth';
4+
import {t} from '../../i18n/index.js';
5+
6+
/**
7+
* JSON output structure for the token command
8+
*/
9+
interface TokenJsonOutput {
10+
accessToken: string;
11+
expires: string;
12+
scopes: string[];
13+
}
14+
15+
export default class AuthToken extends OAuthCommand<typeof AuthToken> {
16+
static description = t('commands.auth.token.description', 'Get an OAuth access token');
17+
18+
static enableJsonFlag = true;
19+
20+
static examples = [
21+
'<%= config.bin %> <%= command.id %>',
22+
'<%= config.bin %> <%= command.id %> --scope sfcc.orders --scope sfcc.products',
23+
'<%= config.bin %> <%= command.id %> --json',
24+
];
25+
26+
async run(): Promise<TokenJsonOutput> {
27+
this.requireOAuthCredentials();
28+
29+
const strategy = this.getOAuthStrategy();
30+
const tokenResponse: AccessTokenResponse = await strategy.getTokenResponse();
31+
32+
const output: TokenJsonOutput = {
33+
accessToken: tokenResponse.accessToken,
34+
expires: tokenResponse.expires.toISOString(),
35+
scopes: tokenResponse.scopes,
36+
};
37+
38+
// In JSON mode, return the full token response
39+
if (this.jsonEnabled()) {
40+
return output;
41+
}
42+
43+
// In normal mode, output just the raw token to stdout
44+
ux.stdout(tokenResponse.accessToken);
45+
46+
return output;
47+
}
48+
}

packages/b2c-tooling/src/auth/oauth.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,31 @@ export class OAuthStrategy implements AuthStrategy {
6767
return decodeJWT(token);
6868
}
6969

70+
/**
71+
* Gets the full token response including expiration and scopes.
72+
* Useful for commands that need to display or return token metadata.
73+
*/
74+
async getTokenResponse(): Promise<AccessTokenResponse> {
75+
const logger = getLogger();
76+
const cached = ACCESS_TOKEN_CACHE.get(this.config.clientId);
77+
78+
if (cached) {
79+
const now = new Date();
80+
const requiredScopes = this.config.scopes || [];
81+
const hasAllScopes = requiredScopes.every((scope) => cached.scopes.includes(scope));
82+
83+
if (hasAllScopes && now.getTime() <= cached.expires.getTime()) {
84+
logger.debug('Reusing cached access token');
85+
return cached;
86+
}
87+
}
88+
89+
// Get new token via client credentials
90+
const tokenResponse = await this.clientCredentialsGrant();
91+
ACCESS_TOKEN_CACHE.set(this.config.clientId, tokenResponse);
92+
return tokenResponse;
93+
}
94+
7095
/**
7196
* Invalidates the cached token, forcing re-authentication on next request
7297
*/

packages/b2c-tooling/src/cli/oauth-command.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@ import {Command, Flags} from '@oclif/core';
22
import {BaseCommand} from './base-command.js';
33
import {loadConfig} from './config.js';
44
import type {ResolvedConfig, LoadConfigOptions} from './config.js';
5-
import type {AuthStrategy} from '../auth/types.js';
65
import {OAuthStrategy} from '../auth/oauth.js';
76
import {t} from '../i18n/index.js';
87

98
/**
109
* Base command for operations requiring OAuth authentication.
11-
* Use this for platform-level operations like ODS/Sandbox API.
10+
* Use this for platform-level operations like ODS, APIs.
1211
*
1312
* Environment variables:
1413
* - SFCC_CLIENT_ID: OAuth client ID
1514
* - SFCC_CLIENT_SECRET: OAuth client secret
1615
*
17-
* For B2C instance operations, use InstanceCommand instead.
16+
* For B2C instance specific operations, use InstanceCommand instead.
1817
*/
1918
export abstract class OAuthCommand<T extends typeof Command> extends BaseCommand<T> {
2019
static baseFlags = {
@@ -29,6 +28,12 @@ export abstract class OAuthCommand<T extends typeof Command> extends BaseCommand
2928
env: 'SFCC_CLIENT_SECRET',
3029
helpGroup: 'AUTH',
3130
}),
31+
scope: Flags.string({
32+
description: 'OAuth scopes to request (can be specified multiple times)',
33+
env: 'SFCC_OAUTH_SCOPES',
34+
multiple: true,
35+
helpGroup: 'AUTH',
36+
}),
3237
};
3338

3439
protected override loadConfiguration(): ResolvedConfig {
@@ -42,13 +47,20 @@ export abstract class OAuthCommand<T extends typeof Command> extends BaseCommand
4247
clientSecret: this.flags['client-secret'],
4348
};
4449

45-
return loadConfig(flagConfig, options);
50+
const config = loadConfig(flagConfig, options);
51+
52+
// Merge scopes from flags with config file scopes (flags take precedence if provided)
53+
if (this.flags.scope && this.flags.scope.length > 0) {
54+
config.scopes = this.flags.scope;
55+
}
56+
57+
return config;
4658
}
4759

4860
/**
4961
* Gets an OAuth auth strategy.
5062
*/
51-
protected getOAuthStrategy(): AuthStrategy {
63+
protected getOAuthStrategy(): OAuthStrategy {
5264
const config = this.resolvedConfig;
5365

5466
if (config.clientId && config.clientSecret) {

0 commit comments

Comments
 (0)