Skip to content

Commit 3f4f23d

Browse files
committed
Adda allowPublicClientFlow option to entra app add/set commands #5870
1 parent 72886a7 commit 3f4f23d

File tree

6 files changed

+207
-3
lines changed

6 files changed

+207
-3
lines changed

docs/docs/cmd/entra/app/app-add.mdx

+9
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ m365 entra appregistration add [options]
7878

7979
`--save`
8080
: Use to store the information about the created app in a local file.
81+
82+
`--allowPublicClientFlows`
83+
: Enable the allow public client flows feature on the app registration.
8184
```
8285

8386
<Global />
@@ -192,6 +195,12 @@ Create new Entra app registration with a certificate
192195
m365 entra app add --name 'My Entra app' --certificateDisplayName "Some certificate name" --certificateFile "c:\temp\some-certificate.cer"
193196
```
194197

198+
Create a new Entra app registration with the allow public client flows feature enabled.
199+
200+
```sh
201+
m365 entra app add --name 'My Entra app' --allowPublicClientFlows
202+
```
203+
195204
## Response
196205

197206
### Standard response

docs/docs/cmd/entra/app/app-set.mdx

+9
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ m365 entra appregistration set [options]
4949

5050
`--certificateDisplayName [certificateDisplayName]`
5151
: Display name for the certificate. If not given, the displayName will be set to the certificate subject. When specified, also specify either `certificateFile` or `certificateBase64Encoded`.
52+
53+
`--allowPublicClientFlows [allowPublicClientFlows]`
54+
: Set to `true` or `false` to toggle the allow public client flows feature on the app registration.
5255
```
5356

5457
<Global />
@@ -99,6 +102,12 @@ Add a certificate to the app
99102
m365 entra app set --appId e75be2e1-0204-4f95-857d-51a37cf40be8 --certificateDisplayName "Some certificate name" --certificateFile "c:\temp\some-certificate.cer"
100103
```
101104

105+
Enable the allow public client flows feature on the app registration
106+
107+
```sh
108+
m365 entra app set --appId e75be2e1-0204-4f95-857d-51a37cf40be8 --allowPublicClientFlows true
109+
```
110+
102111
## Response
103112

104113
The command won't return a response on success.

src/m365/entra/commands/app/app-add.spec.ts

+91
Original file line numberDiff line numberDiff line change
@@ -7793,4 +7793,95 @@ describe(commands.APP_ADD, () => {
77937793
tenantId: ''
77947794
}));
77957795
});
7796+
7797+
it('creates Entra app reg with defined name and allowPublicClientFlows option enabled', async () => {
7798+
sinon.stub(request, 'get').rejects('Issues GET request');
7799+
sinon.stub(request, 'patch').rejects('Issued PATCH request');
7800+
sinon.stub(request, 'post').callsFake(async opts => {
7801+
if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications' &&
7802+
JSON.stringify(opts.data) === JSON.stringify({
7803+
"displayName": "My AAD app",
7804+
"signInAudience": "AzureADMyOrg",
7805+
"isFallbackPublicClient": true
7806+
})) {
7807+
return {
7808+
"id": "5b31c38c-2584-42f0-aa47-657fb3a84230",
7809+
"deletedDateTime": null,
7810+
"appId": "bc724b77-da87-43a9-b385-6ebaaf969db8",
7811+
"applicationTemplateId": null,
7812+
"createdDateTime": "2020-12-31T14:44:13.7945807Z",
7813+
"displayName": "My AAD app",
7814+
"description": null,
7815+
"groupMembershipClaims": null,
7816+
"identifierUris": [],
7817+
"isDeviceOnlyAuthSupported": null,
7818+
"isFallbackPublicClient": true,
7819+
"notes": null,
7820+
"optionalClaims": null,
7821+
"publisherDomain": "contoso.onmicrosoft.com",
7822+
"signInAudience": "AzureADMyOrg",
7823+
"tags": [],
7824+
"tokenEncryptionKeyId": null,
7825+
"verifiedPublisher": {
7826+
"displayName": null,
7827+
"verifiedPublisherId": null,
7828+
"addedDateTime": null
7829+
},
7830+
"spa": {
7831+
"redirectUris": []
7832+
},
7833+
"defaultRedirectUri": null,
7834+
"addIns": [],
7835+
"api": {
7836+
"acceptMappedClaims": null,
7837+
"knownClientApplications": [],
7838+
"requestedAccessTokenVersion": null,
7839+
"oauth2PermissionScopes": [],
7840+
"preAuthorizedApplications": []
7841+
},
7842+
"appRoles": [],
7843+
"info": {
7844+
"logoUrl": null,
7845+
"marketingUrl": null,
7846+
"privacyStatementUrl": null,
7847+
"supportUrl": null,
7848+
"termsOfServiceUrl": null
7849+
},
7850+
"keyCredentials": [],
7851+
"parentalControlSettings": {
7852+
"countriesBlockedForMinors": [],
7853+
"legalAgeGroupRule": "Allow"
7854+
},
7855+
"passwordCredentials": [],
7856+
"publicClient": {
7857+
"redirectUris": []
7858+
},
7859+
"requiredResourceAccess": [],
7860+
"web": {
7861+
"homePageUrl": null,
7862+
"logoutUrl": null,
7863+
"redirectUris": [],
7864+
"implicitGrantSettings": {
7865+
"enableAccessTokenIssuance": false,
7866+
"enableIdTokenIssuance": false
7867+
}
7868+
}
7869+
};
7870+
}
7871+
7872+
throw `Invalid POST request: ${JSON.stringify(opts, null, 2)}`;
7873+
});
7874+
7875+
await command.action(logger, {
7876+
options: {
7877+
name: 'My AAD app',
7878+
allowPublicClientFlows: true
7879+
}
7880+
});
7881+
assert(loggerLogSpy.calledWith({
7882+
appId: 'bc724b77-da87-43a9-b385-6ebaaf969db8',
7883+
objectId: '5b31c38c-2584-42f0-aa47-657fb3a84230',
7884+
tenantId: ''
7885+
}));
7886+
});
77967887
});

src/m365/entra/commands/app/app-add.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ interface Options extends GlobalOptions {
6565
certificateFile?: string;
6666
certificateBase64Encoded?: string;
6767
certificateDisplayName?: string;
68+
allowPublicClientFlows?: boolean;
6869
}
6970

7071
interface AppPermissions {
@@ -118,7 +119,8 @@ class EntraAppAddCommand extends GraphCommand {
118119
certificateFile: typeof args.options.certificateFile !== 'undefined',
119120
certificateBase64Encoded: typeof args.options.certificateBase64Encoded !== 'undefined',
120121
certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined',
121-
grantAdminConsent: typeof args.options.grantAdminConsent !== 'undefined'
122+
grantAdminConsent: typeof args.options.grantAdminConsent !== 'undefined',
123+
allowPublicClientFlows: typeof args.options.allowPublicClientFlows !== 'undefined'
122124
});
123125
});
124126
}
@@ -183,6 +185,9 @@ class EntraAppAddCommand extends GraphCommand {
183185
},
184186
{
185187
option: '--grantAdminConsent'
188+
},
189+
{
190+
option: '--allowPublicClientFlows'
186191
}
187192
);
188193
}
@@ -330,6 +335,10 @@ class EntraAppAddCommand extends GraphCommand {
330335
applicationInfo.keyCredentials = [newKeyCredential];
331336
}
332337

338+
if (args.options.allowPublicClientFlows) {
339+
applicationInfo.isFallbackPublicClient = true;
340+
}
341+
333342
if (this.verbose) {
334343
await logger.logToStderr(`Creating Microsoft Entra app registration...`);
335344
}

src/m365/entra/commands/app/app-set.spec.ts

+48
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,38 @@ describe(commands.APP_SET, () => {
964964
});
965965
});
966966

967+
it('updates allowPublicClientFlows value for the specified appId', async () => {
968+
sinon.stub(request, 'get').callsFake(async opts => {
969+
if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq 'bc724b77-da87-43a9-b385-6ebaaf969db8'&$select=id`) {
970+
return {
971+
value: [{
972+
id: '5b31c38c-2584-42f0-aa47-657fb3a84230'
973+
}]
974+
};
975+
}
976+
977+
throw `Invalid request ${JSON.stringify(opts)}`;
978+
});
979+
sinon.stub(request, 'patch').callsFake(async opts => {
980+
if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications/5b31c38c-2584-42f0-aa47-657fb3a84230' &&
981+
opts.data &&
982+
opts.data.isFallbackPublicClient === true) {
983+
return;
984+
}
985+
986+
throw `Invalid request ${JSON.stringify(opts)}`;
987+
});
988+
989+
await command.action(logger, {
990+
options: {
991+
debug: true,
992+
appId: 'bc724b77-da87-43a9-b385-6ebaaf969db8',
993+
allowPublicClientFlows: true
994+
}
995+
});
996+
});
997+
998+
967999
it('handles error when certificate file cannot be read', async () => {
9681000
sinon.stub(request, 'get').callsFake(async opts => {
9691001
if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications/95cfe30d-ed44-4f9d-b73d-c66560f72e83`) {
@@ -1339,4 +1371,20 @@ describe(commands.APP_SET, () => {
13391371
const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', redirectUris: 'https://foo.com', platform: 'web' } }, commandInfo);
13401372
assert.strictEqual(actual, true);
13411373
});
1374+
1375+
it('passes validation when allowPublicClientFlows is specified as true', async () => {
1376+
const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', allowPublicClientFlows: true } }, commandInfo);
1377+
assert.strictEqual(actual, true);
1378+
});
1379+
1380+
it('passes validation when allowPublicClientFlows is specified as false', async () => {
1381+
const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', allowPublicClientFlows: false } }, commandInfo);
1382+
assert.strictEqual(actual, true);
1383+
});
1384+
1385+
it('passes validation when allowPublicClientFlows is not correct boolean value', async () => {
1386+
const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', allowPublicClientFlows: 'foo' } }, commandInfo);
1387+
assert.strictEqual(actual, true);
1388+
});
1389+
13421390
});

src/m365/entra/commands/app/app-set.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface Options extends GlobalOptions {
2424
certificateFile?: string;
2525
certificateBase64Encoded?: string;
2626
certificateDisplayName?: string;
27+
allowPublicClientFlows?: boolean;
2728
}
2829

2930
class EntraAppSetCommand extends GraphCommand {
@@ -48,6 +49,7 @@ class EntraAppSetCommand extends GraphCommand {
4849
this.#initOptions();
4950
this.#initValidators();
5051
this.#initOptionSets();
52+
this.#initTypes();
5153
}
5254

5355
#initTelemetry(): void {
@@ -62,7 +64,8 @@ class EntraAppSetCommand extends GraphCommand {
6264
uris: typeof args.options.uris !== 'undefined',
6365
certificateFile: typeof args.options.certificateFile !== 'undefined',
6466
certificateBase64Encoded: typeof args.options.certificateBase64Encoded !== 'undefined',
65-
certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined'
67+
certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined',
68+
allowPublicClientFlows: typeof args.options.allowPublicClientFlows !== 'undefined'
6669
});
6770
});
6871
}
@@ -81,7 +84,11 @@ class EntraAppSetCommand extends GraphCommand {
8184
option: '--platform [platform]',
8285
autocomplete: EntraAppSetCommand.aadApplicationPlatform
8386
},
84-
{ option: '--redirectUrisToRemove [redirectUrisToRemove]' }
87+
{ option: '--redirectUrisToRemove [redirectUrisToRemove]' },
88+
{
89+
option: '--allowPublicClientFlows [allowPublicClientFlows]',
90+
autocomplete: ['true', 'false']
91+
}
8592
);
8693
}
8794

@@ -118,13 +125,18 @@ class EntraAppSetCommand extends GraphCommand {
118125
this.optionSets.push({ options: ['appId', 'objectId', 'name'] });
119126
}
120127

128+
#initTypes(): void {
129+
this.types.boolean.push('allowPublicClientFlows');
130+
}
131+
121132
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
122133
await this.showDeprecationWarning(logger, aadCommands.APP_SET, commands.APP_SET);
123134

124135
try {
125136
let objectId = await this.getAppObjectId(args, logger);
126137
objectId = await this.configureUri(args, objectId, logger);
127138
objectId = await this.configureRedirectUris(args, objectId, logger);
139+
objectId = await this.updateAllowPublicClientFlows(args, objectId, logger);
128140
await this.configureCertificate(args, objectId, logger);
129141
}
130142
catch (err: any) {
@@ -171,6 +183,32 @@ class EntraAppSetCommand extends GraphCommand {
171183
return result.id;
172184
}
173185

186+
private async updateAllowPublicClientFlows(args: CommandArgs, objectId: string, logger: Logger): Promise<string> {
187+
if (args.options.allowPublicClientFlows === undefined) {
188+
return objectId;
189+
}
190+
191+
if (this.verbose) {
192+
await logger.logToStderr(`Configuring Entra application AllowPublicClientFlows option...`);
193+
}
194+
195+
const applicationInfo: any = {
196+
isFallbackPublicClient: args.options.allowPublicClientFlows
197+
};
198+
199+
const requestOptions: CliRequestOptions = {
200+
url: `${this.resource}/v1.0/myorganization/applications/${objectId}`,
201+
headers: {
202+
'content-type': 'application/json;odata.metadata=none'
203+
},
204+
responseType: 'json',
205+
data: applicationInfo
206+
};
207+
208+
await request.patch(requestOptions);
209+
return objectId;
210+
}
211+
174212
private async configureUri(args: CommandArgs, objectId: string, logger: Logger): Promise<string> {
175213
if (!args.options.uris) {
176214
return objectId;

0 commit comments

Comments
 (0)