diff --git a/docs/docs/cmd/entra/app/app-add.mdx b/docs/docs/cmd/entra/app/app-add.mdx
index 9c15d294c0..f204a6094c 100644
--- a/docs/docs/cmd/entra/app/app-add.mdx
+++ b/docs/docs/cmd/entra/app/app-add.mdx
@@ -78,6 +78,9 @@ m365 entra appregistration add [options]
`--save`
: Use to store the information about the created app in a local file.
+
+`--allowPublicClientFlows`
+: Enable the allow public client flows feature on the app registration.
```
@@ -192,6 +195,12 @@ Create new Entra app registration with a certificate
m365 entra app add --name 'My Entra app' --certificateDisplayName "Some certificate name" --certificateFile "c:\temp\some-certificate.cer"
```
+Create a new Entra app registration with the allow public client flows feature enabled.
+
+```sh
+m365 entra app add --name 'My Entra app' --allowPublicClientFlows
+```
+
## Response
### Standard response
diff --git a/docs/docs/cmd/entra/app/app-set.mdx b/docs/docs/cmd/entra/app/app-set.mdx
index c10b091c80..ffab68e9b1 100644
--- a/docs/docs/cmd/entra/app/app-set.mdx
+++ b/docs/docs/cmd/entra/app/app-set.mdx
@@ -49,6 +49,9 @@ m365 entra appregistration set [options]
`--certificateDisplayName [certificateDisplayName]`
: 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`.
+
+`--allowPublicClientFlows [allowPublicClientFlows]`
+: Set to true to enable the allow public client flows feature on the app registration.
```
@@ -99,6 +102,12 @@ Add a certificate to the app
m365 entra app set --appId e75be2e1-0204-4f95-857d-51a37cf40be8 --certificateDisplayName "Some certificate name" --certificateFile "c:\temp\some-certificate.cer"
```
+Enable the allow public client flows feature on the app registration
+
+```sh
+m365 entra app set --appId e75be2e1-0204-4f95-857d-51a37cf40be8 --allowPublicClientFlows true
+```
+
## Response
The command won't return a response on success.
diff --git a/src/m365/entra/commands/app/app-add.spec.ts b/src/m365/entra/commands/app/app-add.spec.ts
index cfa054e0f3..d220758ad2 100644
--- a/src/m365/entra/commands/app/app-add.spec.ts
+++ b/src/m365/entra/commands/app/app-add.spec.ts
@@ -7793,4 +7793,95 @@ describe(commands.APP_ADD, () => {
tenantId: ''
}));
});
+
+ it('creates Entra app reg with defined name and allowPublicClientFlows option enabled', async () => {
+ sinon.stub(request, 'get').rejects('Issues GET request');
+ sinon.stub(request, 'patch').rejects('Issued PATCH request');
+ sinon.stub(request, 'post').callsFake(async opts => {
+ if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications' &&
+ JSON.stringify(opts.data) === JSON.stringify({
+ "displayName": "My AAD app",
+ "signInAudience": "AzureADMyOrg",
+ "isFallbackPublicClient": true
+ })) {
+ return {
+ "id": "5b31c38c-2584-42f0-aa47-657fb3a84230",
+ "deletedDateTime": null,
+ "appId": "bc724b77-da87-43a9-b385-6ebaaf969db8",
+ "applicationTemplateId": null,
+ "createdDateTime": "2020-12-31T14:44:13.7945807Z",
+ "displayName": "My AAD app",
+ "description": null,
+ "groupMembershipClaims": null,
+ "identifierUris": [],
+ "isDeviceOnlyAuthSupported": null,
+ "isFallbackPublicClient": true,
+ "notes": null,
+ "optionalClaims": null,
+ "publisherDomain": "contoso.onmicrosoft.com",
+ "signInAudience": "AzureADMyOrg",
+ "tags": [],
+ "tokenEncryptionKeyId": null,
+ "verifiedPublisher": {
+ "displayName": null,
+ "verifiedPublisherId": null,
+ "addedDateTime": null
+ },
+ "spa": {
+ "redirectUris": []
+ },
+ "defaultRedirectUri": null,
+ "addIns": [],
+ "api": {
+ "acceptMappedClaims": null,
+ "knownClientApplications": [],
+ "requestedAccessTokenVersion": null,
+ "oauth2PermissionScopes": [],
+ "preAuthorizedApplications": []
+ },
+ "appRoles": [],
+ "info": {
+ "logoUrl": null,
+ "marketingUrl": null,
+ "privacyStatementUrl": null,
+ "supportUrl": null,
+ "termsOfServiceUrl": null
+ },
+ "keyCredentials": [],
+ "parentalControlSettings": {
+ "countriesBlockedForMinors": [],
+ "legalAgeGroupRule": "Allow"
+ },
+ "passwordCredentials": [],
+ "publicClient": {
+ "redirectUris": []
+ },
+ "requiredResourceAccess": [],
+ "web": {
+ "homePageUrl": null,
+ "logoutUrl": null,
+ "redirectUris": [],
+ "implicitGrantSettings": {
+ "enableAccessTokenIssuance": false,
+ "enableIdTokenIssuance": false
+ }
+ }
+ };
+ }
+
+ throw `Invalid POST request: ${JSON.stringify(opts, null, 2)}`;
+ });
+
+ await command.action(logger, {
+ options: {
+ name: 'My AAD app',
+ allowPublicClientFlows: true
+ }
+ });
+ assert(loggerLogSpy.calledWith({
+ appId: 'bc724b77-da87-43a9-b385-6ebaaf969db8',
+ objectId: '5b31c38c-2584-42f0-aa47-657fb3a84230',
+ tenantId: ''
+ }));
+ });
});
diff --git a/src/m365/entra/commands/app/app-add.ts b/src/m365/entra/commands/app/app-add.ts
index 4b3cf4a8ae..4f512a32ef 100644
--- a/src/m365/entra/commands/app/app-add.ts
+++ b/src/m365/entra/commands/app/app-add.ts
@@ -65,6 +65,7 @@ interface Options extends GlobalOptions {
certificateFile?: string;
certificateBase64Encoded?: string;
certificateDisplayName?: string;
+ allowPublicClientFlows?: boolean;
}
interface AppPermissions {
@@ -118,7 +119,8 @@ class EntraAppAddCommand extends GraphCommand {
certificateFile: typeof args.options.certificateFile !== 'undefined',
certificateBase64Encoded: typeof args.options.certificateBase64Encoded !== 'undefined',
certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined',
- grantAdminConsent: typeof args.options.grantAdminConsent !== 'undefined'
+ grantAdminConsent: typeof args.options.grantAdminConsent !== 'undefined',
+ allowPublicClientFlows: typeof args.options.allowPublicClientFlows !== 'undefined'
});
});
}
@@ -183,6 +185,9 @@ class EntraAppAddCommand extends GraphCommand {
},
{
option: '--grantAdminConsent'
+ },
+ {
+ option: '--allowPublicClientFlows'
}
);
}
@@ -328,6 +333,10 @@ class EntraAppAddCommand extends GraphCommand {
applicationInfo.keyCredentials = [newKeyCredential];
}
+ if (args.options.allowPublicClientFlows) {
+ applicationInfo.isFallbackPublicClient = true;
+ }
+
if (this.verbose) {
await logger.logToStderr(`Creating Azure AD app registration...`);
}
diff --git a/src/m365/entra/commands/app/app-set.spec.ts b/src/m365/entra/commands/app/app-set.spec.ts
index 2aab488654..35d4e1a00c 100644
--- a/src/m365/entra/commands/app/app-set.spec.ts
+++ b/src/m365/entra/commands/app/app-set.spec.ts
@@ -964,6 +964,38 @@ describe(commands.APP_SET, () => {
});
});
+ it('updates allowPublicClientFlows value for the specified appId', async () => {
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq 'bc724b77-da87-43a9-b385-6ebaaf969db8'&$select=id`) {
+ return {
+ value: [{
+ id: '5b31c38c-2584-42f0-aa47-657fb3a84230'
+ }]
+ };
+ }
+
+ throw `Invalid request ${JSON.stringify(opts)}`;
+ });
+ sinon.stub(request, 'patch').callsFake(async opts => {
+ if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications/5b31c38c-2584-42f0-aa47-657fb3a84230' &&
+ opts.data &&
+ opts.data.isFallbackPublicClient === true) {
+ return;
+ }
+
+ throw `Invalid request ${JSON.stringify(opts)}`;
+ });
+
+ await command.action(logger, {
+ options: {
+ debug: true,
+ appId: 'bc724b77-da87-43a9-b385-6ebaaf969db8',
+ allowPublicClientFlows: true
+ }
+ });
+ });
+
+
it('handles error when certificate file cannot be read', async () => {
sinon.stub(request, 'get').callsFake(async opts => {
if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications/95cfe30d-ed44-4f9d-b73d-c66560f72e83`) {
@@ -1339,4 +1371,20 @@ describe(commands.APP_SET, () => {
const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', redirectUris: 'https://foo.com', platform: 'web' } }, commandInfo);
assert.strictEqual(actual, true);
});
+
+ it('passes validation when allowPublicClientFlows is specified as true', async () => {
+ const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', allowPublicClientFlows: true } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('passes validation when allowPublicClientFlows is specified as false', async () => {
+ const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', allowPublicClientFlows: false } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('passes validation when allowPublicClientFlows is not correct boolean value', async () => {
+ const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', allowPublicClientFlows: 'foo' } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
});
diff --git a/src/m365/entra/commands/app/app-set.ts b/src/m365/entra/commands/app/app-set.ts
index 7602484df1..052e4e7bcb 100644
--- a/src/m365/entra/commands/app/app-set.ts
+++ b/src/m365/entra/commands/app/app-set.ts
@@ -24,6 +24,7 @@ interface Options extends GlobalOptions {
certificateFile?: string;
certificateBase64Encoded?: string;
certificateDisplayName?: string;
+ allowPublicClientFlows?: boolean;
}
class EntraAppSetCommand extends GraphCommand {
@@ -48,6 +49,7 @@ class EntraAppSetCommand extends GraphCommand {
this.#initOptions();
this.#initValidators();
this.#initOptionSets();
+ this.#initTypes();
}
#initTelemetry(): void {
@@ -62,7 +64,8 @@ class EntraAppSetCommand extends GraphCommand {
uris: typeof args.options.uris !== 'undefined',
certificateFile: typeof args.options.certificateFile !== 'undefined',
certificateBase64Encoded: typeof args.options.certificateBase64Encoded !== 'undefined',
- certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined'
+ certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined',
+ allowPublicClientFlows: typeof args.options.allowPublicClientFlows !== 'undefined'
});
});
}
@@ -81,7 +84,11 @@ class EntraAppSetCommand extends GraphCommand {
option: '--platform [platform]',
autocomplete: EntraAppSetCommand.aadApplicationPlatform
},
- { option: '--redirectUrisToRemove [redirectUrisToRemove]' }
+ { option: '--redirectUrisToRemove [redirectUrisToRemove]' },
+ {
+ option: '--allowPublicClientFlows [allowPublicClientFlows]',
+ autocomplete: ['true', 'false']
+ }
);
}
@@ -118,11 +125,16 @@ class EntraAppSetCommand extends GraphCommand {
this.optionSets.push({ options: ['appId', 'objectId', 'name'] });
}
+ #initTypes(): void {
+ this.types.boolean.push('allowPublicClientFlows');
+ }
+
public async commandAction(logger: Logger, args: CommandArgs): Promise {
try {
let objectId = await this.getAppObjectId(args, logger);
objectId = await this.configureUri(args, objectId, logger);
objectId = await this.configureRedirectUris(args, objectId, logger);
+ objectId = await this.updateAllowPublicClientFlows(args, objectId, logger);
await this.configureCertificate(args, objectId, logger);
}
catch (err: any) {
@@ -169,6 +181,32 @@ class EntraAppSetCommand extends GraphCommand {
return result.id;
}
+ private async updateAllowPublicClientFlows(args: CommandArgs, objectId: string, logger: Logger): Promise {
+ if (args.options.allowPublicClientFlows === undefined) {
+ return objectId;
+ }
+
+ if (this.verbose) {
+ await logger.logToStderr(`Configuring Entra application AllowPublicClientFlows option...`);
+ }
+
+ const applicationInfo: any = {
+ isFallbackPublicClient: args.options.allowPublicClientFlows
+ };
+
+ const requestOptions: CliRequestOptions = {
+ url: `${this.resource}/v1.0/myorganization/applications/${objectId}`,
+ headers: {
+ 'content-type': 'application/json;odata.metadata=none'
+ },
+ responseType: 'json',
+ data: applicationInfo
+ };
+
+ await request.patch(requestOptions);
+ return objectId;
+ }
+
private async configureUri(args: CommandArgs, objectId: string, logger: Logger): Promise {
if (!args.options.uris) {
return objectId;