Skip to content

Commit

Permalink
Adda allowPublicClientFlow option to entra app add/set commands #5870
Browse files Browse the repository at this point in the history
  • Loading branch information
mkm17 committed Apr 2, 2024
1 parent 72886a7 commit 3f4f23d
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 3 deletions.
9 changes: 9 additions & 0 deletions docs/docs/cmd/entra/app/app-add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```

<Global />
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/cmd/entra/app/app-set.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` or `false` to toggle the allow public client flows feature on the app registration.
```

<Global />
Expand Down Expand Up @@ -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.
91 changes: 91 additions & 0 deletions src/m365/entra/commands/app/app-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''
}));
});
});
11 changes: 10 additions & 1 deletion src/m365/entra/commands/app/app-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ interface Options extends GlobalOptions {
certificateFile?: string;
certificateBase64Encoded?: string;
certificateDisplayName?: string;
allowPublicClientFlows?: boolean;
}

interface AppPermissions {
Expand Down Expand Up @@ -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'
});
});
}
Expand Down Expand Up @@ -183,6 +185,9 @@ class EntraAppAddCommand extends GraphCommand {
},
{
option: '--grantAdminConsent'
},
{
option: '--allowPublicClientFlows'
}
);
}
Expand Down Expand Up @@ -330,6 +335,10 @@ class EntraAppAddCommand extends GraphCommand {
applicationInfo.keyCredentials = [newKeyCredential];
}

if (args.options.allowPublicClientFlows) {
applicationInfo.isFallbackPublicClient = true;
}

if (this.verbose) {
await logger.logToStderr(`Creating Microsoft Entra app registration...`);
}
Expand Down
48 changes: 48 additions & 0 deletions src/m365/entra/commands/app/app-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`) {
Expand Down Expand Up @@ -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);
});

});
42 changes: 40 additions & 2 deletions src/m365/entra/commands/app/app-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface Options extends GlobalOptions {
certificateFile?: string;
certificateBase64Encoded?: string;
certificateDisplayName?: string;
allowPublicClientFlows?: boolean;
}

class EntraAppSetCommand extends GraphCommand {
Expand All @@ -48,6 +49,7 @@ class EntraAppSetCommand extends GraphCommand {
this.#initOptions();
this.#initValidators();
this.#initOptionSets();
this.#initTypes();
}

#initTelemetry(): void {
Expand All @@ -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'
});
});
}
Expand All @@ -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']
}
);
}

Expand Down Expand Up @@ -118,13 +125,18 @@ 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<void> {
await this.showDeprecationWarning(logger, aadCommands.APP_SET, commands.APP_SET);

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) {
Expand Down Expand Up @@ -171,6 +183,32 @@ class EntraAppSetCommand extends GraphCommand {
return result.id;
}

private async updateAllowPublicClientFlows(args: CommandArgs, objectId: string, logger: Logger): Promise<string> {
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<string> {
if (!args.options.uris) {
return objectId;
Expand Down

0 comments on commit 3f4f23d

Please sign in to comment.