Skip to content

Commit c747d62

Browse files
@W-20893800: Adding support for stateful auth [sfcc-ci compatibility]
1 parent ee34526 commit c747d62

File tree

4 files changed

+136
-45
lines changed

4 files changed

+136
-45
lines changed

packages/b2c-cli/src/commands/auth/client/index.ts

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
66
import {Flags} from '@oclif/core';
7-
import {OAuthCommand} from '@salesforce/b2c-tooling-sdk/cli';
7+
import {BaseCommand, loadConfig} from '@salesforce/b2c-tooling-sdk/cli';
88
import {setStoredSession, decodeJWT} from '@salesforce/b2c-tooling-sdk/auth';
9+
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '@salesforce/b2c-tooling-sdk';
910
import {t} from '../../../i18n/index.js';
1011

1112
/**
@@ -19,7 +20,7 @@ import {t} from '../../../i18n/index.js';
1920
*
2021
* Use --renew to enable automatic token renewal for later use with `auth client renew`.
2122
*/
22-
export default class AuthClient extends OAuthCommand<typeof AuthClient> {
23+
export default class AuthClient extends BaseCommand<typeof AuthClient> {
2324
static description = t('commands.auth.client.description', 'Authenticate an API client and save session');
2425

2526
static examples = [
@@ -30,6 +31,29 @@ export default class AuthClient extends OAuthCommand<typeof AuthClient> {
3031
];
3132

3233
static flags = {
34+
'client-id': Flags.string({
35+
description: 'Client ID for OAuth',
36+
env: 'SFCC_CLIENT_ID',
37+
helpGroup: 'AUTH',
38+
}),
39+
'client-secret': Flags.string({
40+
description: 'Client secret for OAuth',
41+
env: 'SFCC_CLIENT_SECRET',
42+
helpGroup: 'AUTH',
43+
}),
44+
'account-manager-host': Flags.string({
45+
description: `Account Manager hostname for OAuth (default: ${DEFAULT_ACCOUNT_MANAGER_HOST})`,
46+
env: 'SFCC_ACCOUNT_MANAGER_HOST',
47+
helpGroup: 'AUTH',
48+
}),
49+
'auth-scope': Flags.string({
50+
description: 'OAuth scopes to request (comma-separated)',
51+
env: 'SFCC_OAUTH_SCOPES',
52+
multiple: true,
53+
multipleNonGreedy: true,
54+
delimiter: ',',
55+
helpGroup: 'AUTH',
56+
}),
3357
renew: Flags.boolean({
3458
char: 'r',
3559
description: 'Enable automatic token renewal (stores credentials for later refresh)',
@@ -50,6 +74,20 @@ export default class AuthClient extends OAuthCommand<typeof AuthClient> {
5074
}),
5175
};
5276

77+
protected override loadConfiguration() {
78+
const scopes = this.flags['auth-scope'] as string[] | undefined;
79+
return loadConfig(
80+
{
81+
clientId: this.flags['client-id'] as string | undefined,
82+
clientSecret: this.flags['client-secret'] as string | undefined,
83+
accountManagerHost: this.flags['account-manager-host'] as string | undefined,
84+
scopes: scopes && scopes.length > 0 ? scopes : undefined,
85+
},
86+
this.getBaseConfigOptions(),
87+
this.getPluginSources(),
88+
);
89+
}
90+
5391
async run(): Promise<void> {
5492
const clientId = this.resolvedConfig.values.clientId;
5593
const clientSecret = this.resolvedConfig.values.clientSecret;
@@ -65,19 +103,9 @@ export default class AuthClient extends OAuthCommand<typeof AuthClient> {
65103

66104
const user = this.flags.user;
67105
const userPassword = this.flags['user-password'];
68-
const grantTypeFlag = this.flags['grant-type'];
106+
const grantType = this.resolveGrantType(this.flags['grant-type'], user);
69107
const autoRenew = this.flags.renew;
70108

71-
// Determine grant type: explicit flag > auto-detect from user/password > client_credentials
72-
let grantType: string;
73-
if (grantTypeFlag) {
74-
grantType = grantTypeFlag;
75-
} else if (user) {
76-
grantType = 'password';
77-
} else {
78-
grantType = 'client_credentials';
79-
}
80-
81109
if (grantType === 'password' && (!user || !userPassword)) {
82110
this.error(
83111
t(
@@ -87,7 +115,7 @@ export default class AuthClient extends OAuthCommand<typeof AuthClient> {
87115
);
88116
}
89117

90-
const accountManagerHost = this.accountManagerHost;
118+
const accountManagerHost = this.resolvedConfig.values.accountManagerHost ?? DEFAULT_ACCOUNT_MANAGER_HOST;
91119
const scopes = this.resolvedConfig.values.scopes;
92120

93121
const grantPayload: Record<string, string> = {grant_type: grantType};
@@ -128,14 +156,11 @@ export default class AuthClient extends OAuthCommand<typeof AuthClient> {
128156
if (!response.ok) {
129157
const errorText = await response.text();
130158
this.logger.trace({method, url, body: errorText}, `[StatefulAuth RESP BODY] ${method} ${url}`);
131-
let errorMsg: string;
132-
try {
133-
const parsed = JSON.parse(errorText) as {error_description?: string};
134-
errorMsg = parsed.error_description ?? errorText;
135-
} catch {
136-
errorMsg = errorText;
137-
}
138-
this.error(t('commands.auth.client.failed', 'Authentication failed: {{error}}', {error: errorMsg}));
159+
this.error(
160+
t('commands.auth.client.failed', 'Authentication failed: {{error}}', {
161+
error: this.parseErrorMessage(errorText),
162+
}),
163+
);
139164
}
140165

141166
const data = (await response.json()) as {
@@ -148,28 +173,40 @@ export default class AuthClient extends OAuthCommand<typeof AuthClient> {
148173

149174
this.logger.trace({method, url, body: data}, `[StatefulAuth RESP BODY] ${method} ${url}`);
150175

151-
// Extract user from id_token JWT if issued (matches sfcc-ci behavior)
152-
let sessionUser: null | string = null;
153-
if (data.id_token) {
154-
try {
155-
const decoded = decodeJWT(data.id_token);
156-
if (typeof decoded.payload.sub === 'string') {
157-
sessionUser = decoded.payload.sub;
158-
}
159-
} catch {
160-
// Ignore JWT decode errors for id_token
161-
}
162-
}
163-
164176
setStoredSession({
165177
clientId,
166178
accessToken: data.access_token,
167179
refreshToken: data.refresh_token ?? null,
168180
renewBase: autoRenew ? credentials : null,
169-
user: sessionUser,
181+
user: this.extractUser(data.id_token),
170182
});
171183

172184
const renewMsg = autoRenew ? ' Auto-renewal enabled.' : '';
173185
this.log(t('commands.auth.client.success', 'Authentication succeeded.{{renewMsg}}', {renewMsg}));
174186
}
187+
188+
private extractUser(idToken: string | undefined): null | string {
189+
if (!idToken) return null;
190+
try {
191+
const decoded = decodeJWT(idToken);
192+
return typeof decoded.payload.sub === 'string' ? decoded.payload.sub : null;
193+
} catch {
194+
return null;
195+
}
196+
}
197+
198+
private parseErrorMessage(errorText: string): string {
199+
try {
200+
const parsed = JSON.parse(errorText) as {error_description?: string};
201+
return parsed.error_description ?? errorText;
202+
} catch {
203+
return errorText;
204+
}
205+
}
206+
207+
private resolveGrantType(grantTypeFlag: string | undefined, user: string | undefined): string {
208+
if (grantTypeFlag) return grantTypeFlag;
209+
if (user) return 'password';
210+
return 'client_credentials';
211+
}
175212
}

packages/b2c-cli/src/commands/auth/client/renew.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6-
import {OAuthCommand} from '@salesforce/b2c-tooling-sdk/cli';
6+
import {Flags} from '@oclif/core';
7+
import {BaseCommand, loadConfig} from '@salesforce/b2c-tooling-sdk/cli';
78
import {getStoredSession, setStoredSession} from '@salesforce/b2c-tooling-sdk/auth';
9+
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '@salesforce/b2c-tooling-sdk';
810
import {t} from '../../../i18n/index.js';
911

1012
/**
@@ -16,11 +18,27 @@ import {t} from '../../../i18n/index.js';
1618
* Uses refresh_token grant when a refresh token is stored, otherwise falls back
1719
* to client_credentials grant using the stored base64-encoded client:secret.
1820
*/
19-
export default class AuthClientRenew extends OAuthCommand<typeof AuthClientRenew> {
21+
export default class AuthClientRenew extends BaseCommand<typeof AuthClientRenew> {
2022
static description = t('commands.auth.client.renew.description', 'Renew the client authentication token');
2123

2224
static examples = ['<%= config.bin %> <%= command.id %>'];
2325

26+
static flags = {
27+
'account-manager-host': Flags.string({
28+
description: `Account Manager hostname for OAuth (default: ${DEFAULT_ACCOUNT_MANAGER_HOST})`,
29+
env: 'SFCC_ACCOUNT_MANAGER_HOST',
30+
helpGroup: 'AUTH',
31+
}),
32+
};
33+
34+
protected override loadConfiguration() {
35+
return loadConfig(
36+
{accountManagerHost: this.flags['account-manager-host'] as string | undefined},
37+
this.getBaseConfigOptions(),
38+
this.getPluginSources(),
39+
);
40+
}
41+
2442
async run(): Promise<void> {
2543
const session = getStoredSession();
2644

@@ -33,7 +51,7 @@ export default class AuthClientRenew extends OAuthCommand<typeof AuthClientRenew
3351
);
3452
}
3553

36-
const accountManagerHost = this.accountManagerHost;
54+
const accountManagerHost = this.resolvedConfig.values.accountManagerHost ?? DEFAULT_ACCOUNT_MANAGER_HOST;
3755
const url = `https://${accountManagerHost}/dwsso/oauth2/access_token`;
3856

3957
// Use refresh_token grant if available, otherwise client_credentials

packages/b2c-cli/src/commands/auth/login.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6-
import {OAuthCommand} from '@salesforce/b2c-tooling-sdk/cli';
6+
import {Flags} from '@oclif/core';
7+
import {BaseCommand, loadConfig} from '@salesforce/b2c-tooling-sdk/cli';
78
import {ImplicitOAuthStrategy, setStoredSession, decodeJWT} from '@salesforce/b2c-tooling-sdk/auth';
9+
import {DEFAULT_ACCOUNT_MANAGER_HOST} from '@salesforce/b2c-tooling-sdk';
810
import {t, withDocs} from '../../i18n/index.js';
911

1012
/**
1113
* Log in via browser (implicit OAuth) and persist the session for stateful auth.
1214
* Uses the same storage as sfcc-ci; when valid, subsequent commands use this token
1315
* until it expires or you run auth:logout.
1416
*/
15-
export default class AuthLogin extends OAuthCommand<typeof AuthLogin> {
17+
export default class AuthLogin extends BaseCommand<typeof AuthLogin> {
1618
static description = withDocs(
1719
t('commands.auth.login.description', 'Log in via browser and save session (stateful auth)'),
1820
'/cli/auth.html#b2c-auth-login',
@@ -23,15 +25,49 @@ export default class AuthLogin extends OAuthCommand<typeof AuthLogin> {
2325
'<%= config.bin %> <%= command.id %> --client-id your-client-id',
2426
];
2527

28+
static flags = {
29+
'client-id': Flags.string({
30+
description: 'Client ID for OAuth',
31+
env: 'SFCC_CLIENT_ID',
32+
helpGroup: 'AUTH',
33+
}),
34+
'account-manager-host': Flags.string({
35+
description: `Account Manager hostname for OAuth (default: ${DEFAULT_ACCOUNT_MANAGER_HOST})`,
36+
env: 'SFCC_ACCOUNT_MANAGER_HOST',
37+
helpGroup: 'AUTH',
38+
}),
39+
'auth-scope': Flags.string({
40+
description: 'OAuth scopes to request (comma-separated)',
41+
env: 'SFCC_OAUTH_SCOPES',
42+
multiple: true,
43+
multipleNonGreedy: true,
44+
delimiter: ',',
45+
helpGroup: 'AUTH',
46+
}),
47+
};
48+
49+
protected override loadConfiguration() {
50+
const scopes = this.flags['auth-scope'] as string[] | undefined;
51+
return loadConfig(
52+
{
53+
clientId: this.flags['client-id'] as string | undefined,
54+
accountManagerHost: this.flags['account-manager-host'] as string | undefined,
55+
scopes: scopes && scopes.length > 0 ? scopes : undefined,
56+
},
57+
this.getBaseConfigOptions(),
58+
this.getPluginSources(),
59+
);
60+
}
61+
2662
async run(): Promise<void> {
27-
const clientId = this.resolvedConfig.values.clientId ?? this.getDefaultClientId();
63+
const clientId = this.resolvedConfig.values.clientId;
2864
if (!clientId) {
2965
this.error(
3066
t('error.oauthClientIdRequired', 'OAuth client ID required. Provide --client-id or set SFCC_CLIENT_ID.'),
3167
);
3268
}
3369

34-
const accountManagerHost = this.accountManagerHost;
70+
const accountManagerHost = this.resolvedConfig.values.accountManagerHost ?? DEFAULT_ACCOUNT_MANAGER_HOST;
3571
const scopes = this.resolvedConfig.values.scopes;
3672

3773
const strategy = new ImplicitOAuthStrategy({

packages/b2c-cli/src/commands/auth/logout.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6-
import {OAuthCommand} from '@salesforce/b2c-tooling-sdk/cli';
6+
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
77
import {clearStoredSession} from '@salesforce/b2c-tooling-sdk/auth';
88
import {t, withDocs} from '../../i18n/index.js';
99

@@ -12,7 +12,7 @@ import {t, withDocs} from '../../i18n/index.js';
1212
* Uses the same storage as sfcc-ci; after logout, commands use stateless auth
1313
* (client credentials or implicit) when configured.
1414
*/
15-
export default class AuthLogout extends OAuthCommand<typeof AuthLogout> {
15+
export default class AuthLogout extends BaseCommand<typeof AuthLogout> {
1616
static description = withDocs(
1717
t('commands.auth.logout.description', 'Clear stored session (stateful auth)'),
1818
'/cli/auth.html#b2c-auth-logout',

0 commit comments

Comments
 (0)