Skip to content

Commit

Permalink
Merge pull request #691 from adobe/issues/oauth-authentication
Browse files Browse the repository at this point in the history
fix(auth): add support for OAuth authentication in parallel to JWT
  • Loading branch information
kritikash18 authored May 7, 2024
2 parents 30b3d18 + 3a401af commit 0ac4a27
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 16 deletions.
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,40 @@ Alternatively, if you have selected an organization using `aio console:org:selec

To use a service account authentication, an integration (aka project) must be created in the [Adobe I/O Console](https://console.adobe.io) which has the Cloud Manager service.

***The required type of server-to-server authentication should be [Service Account (JWT)](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication/#service-account-jwt-credential-deprecated).***
***The required type of server-to-server authentication should be [Service Account (JWT/OAuth)](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication).***
***NOTE:*** The JWT mode of authentication is deprecated and will be completely removed by Jan,2025. So if you are using JWT integration, it is recommended to migrate to OAuth

#### Setup for OAuth integration

After you've created the integration, create a `config.json` file on your computer and navigate to the integration Overview page. From this page, copy the values into the file as described below.
```
//config.json
{
"client_id": "value from your CLI integration (String)",
"client_secret": "value from your CLI integration (String)",
"technical_account_id": "value from your CLI integration (String)",
"technical_account_email": "value from your CLI integration (String)",
"ims_org_id": "value from your CLI integration (String)",
"scopes": [
'openid',
'AdobeID',
'read_organizations',
'additional_info.projectedProductContext',
'read_pc.dma_aem_ams'
],
"oauth_enabled": true
}
```

Configure the credentials:

```
aio config:set ims.contexts.aio-cli-plugin-cloudmanager PATH_TO_CONFIG_JSON_FILE --file --json
```

#### Setup for JWT integration

After you've created the integration, create a `config.json` file on your computer and navigate to the integration Overview page. From this page, copy the values into the file as described below.
```
//config.json
{
Expand All @@ -83,7 +113,8 @@ After you've created the integration, create a `config.json` file on your comput
"ims_org_id": "value from your CLI integration (String)",
"meta_scopes": [
"ent_cloudmgr_sdk"
]
],
"oauth_enabled": false
}
```

Expand Down Expand Up @@ -1371,6 +1402,16 @@ Note that the private key **must** be base64 encoded, e.g. by running
$ base64 -i private.key
```

To run tests with OAuth credentials, add the following to `.env`:

```
OAUTH_E2E_CLIENT_ID=<CLIENT ID>
OAUTH_E2E_CLIENT_SECRET=<CLIENT SECRET>
OAUTH_E2E_TA_ID=<TECHNICAL ACCOUNT ID>
OAUTH_E2E_TA_EMAIL=<TECHNICAL ACCOUNT EMAIL>
OAUTH_E2E_IMS_ORG_ID=<ORG ID>
```

With this in place the end-to-end tests can be run with

```
Expand Down
62 changes: 58 additions & 4 deletions e2e/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ const exec = (cmd, args) => {
/*
Used in list-programs, which is stubbed out.
Env vars need to be defined and code enabled
Test using JWT integrations; will be removed when JWT is discontinued
*/
const bootstrapAuthContext = async () => {
const bootstrapAuthContextWithJWTIntegration = async () => {
const contextObj = {
client_id: process.env.E2E_CLIENT_ID,
client_secret: process.env.E2E_CLIENT_SECRET,
Expand All @@ -54,6 +55,32 @@ const bootstrapAuthContext = async () => {
'ent_cloudmgr_sdk',
],
// private_key: Buffer.from(process.env.E2E_PRIVATE_KEY_B64, 'base64').toString(),
oauth_enabled: false,
}

await context.set(CONTEXT_NAME, contextObj)
}

/*
Used in list-programs, which is stubbed out.
Env vars need to be defined and code enabled
Test using OAuth integrations
*/
const bootstrapAuthContextWithOAuthIntegration = async () => {
const contextObj = {
client_id: process.env.OAUTH_E2E_CLIENT_ID,
client_secrets: [process.env.OAUTH_E2E_CLIENT_SECRET],
technical_account_id: process.env.OAUTH_E2E_TA_ID,
technical_account_email: process.env.OAUTH_E2E_TA_EMAIL,
ims_org_id: process.env.OAUTH_E2E_IMS_ORG_ID,
scopes: [
'openid',
'AdobeID',
'read_organizations',
'additional_info.projectedProductContext',
'read_pc.dma_aem_ams',
],
oauth_enabled: true,
}

await context.set(CONTEXT_NAME, contextObj)
Expand All @@ -76,11 +103,38 @@ test('plugin-cloudmanager help test', async () => {
*/
/*
* Note: this test cannot be run by the bot, since it requires setup which the bot can't provide
* If wanting to rn the test, the evironment variables have to be set with the required authentication information
* If wanting to run the test, the environment variables have to be set with the required authentication information
* Uses JWT integration which is deprecated; will be removed when JWT is discontinued
*/

test('plugin-cloudmanager list-programs', async () => {
await bootstrapAuthContext()
test('plugin-cloudmanager list-programs using JWT integration', async () => {
await bootstrapAuthContextWithJWTIntegration()
const packagejson = JSON.parse(fs.readFileSync('package.json').toString())
const name = `${packagejson.name}`
console.log(chalk.blue(`> e2e tests for ${chalk.bold(name)}`))

console.log(chalk.dim(' - plugin-cloudmanager list-programs ..'))

// let result
// expect(() => { result = exec('./bin/run', ['cloudmanager:list-programs', ...CONTEXT_ARGS, '--json']) }).not.toThrow()
// const parsed = JSON.parse(result.stdout)
const parsed = '{}'
expect(parsed).toSatisfy(arr => arr.length > 0)

console.log(chalk.green(` - done for ${chalk.bold(name)}`))
})

/*
Side condition: debug log output must not be enabled (DEBUG=* or LOG_LEVEL=debug),
or else the result in result.stdout is not valid JSON and cannot be parsed (line: JSON.parse...)
*/
/*
* Note: this test cannot be run by the bot, since it requires setup which the bot can't provide
* If wanting to run the test, the environment variables have to be set with the required authentication information
* Uses OAuth integrations
*/
test('plugin-cloudmanager list-programs using OAuth integration', async () => {
await bootstrapAuthContextWithOAuthIntegration()
const packagejson = JSON.parse(fs.readFileSync('package.json').toString())
const name = `${packagejson.name}`
console.log(chalk.blue(`> e2e tests for ${chalk.bold(name)}`))
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"@adobe/aio-lib-core-errors": "^3.1.1",
"@adobe/aio-lib-core-logging": "^2.0.0",
"@adobe/aio-lib-core-networking": "^3.0.0",
"@adobe/aio-lib-ims": "^6.0.1",
"@adobe/aio-lib-ims": "^6.5.0",
"@oclif/command": "^1.6.1",
"@oclif/config": "^1.15.1",
"@oclif/parser": "^3.8.5",
Expand Down Expand Up @@ -203,4 +203,4 @@
"@semantic-release/github"
]
}
}
}
1 change: 1 addition & 0 deletions src/ConfigurationErrors.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ E('CLI_AUTH_NO_ORG', 'The CLI has been authenticated, but no organization has be
E('NO_DEFAULT_IMS_CONTEXT', 'There is no IMS context configuration defined for %s. Either define this context configuration or authenticate using "aio auth:login" and select an organization using "aio cloudmanager:org:select".')
E('IMS_CONTEXT_MISSING_FIELDS', 'One or more of the required fields in %s were not set. Missing keys were %s.')
E('IMS_CONTEXT_MISSING_METASCOPE', 'The configuration %s is missing the required metascope %s.')
E('IMS_CONTEXT_MISSING_OAUTH_SCOPES', 'The configuration %s is missing the required OAuth scopes %s.')
E('CLI_AUTH_EXPLICIT_NO_AUTH', 'cli context explicitly enabled, but not authenticated. You must run "aio auth:login" first.')
E('CLI_AUTH_EXPLICIT_NO_ORG', 'cli context explicitly enabled but no org id specified. Configure using either "cloudmanager_orgid" or by running "aio cloudmanager:org:select"')
E('CLI_AUTH_CONTEXT_CANNOT_DECODE', 'The access token configured for cli authentication cannot be decoded.')
Expand Down
21 changes: 15 additions & 6 deletions src/hooks/prerun/check-ims-context-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ const { isThisPlugin } = require('../../cloudmanager-hook-helpers')
const { defaultImsContextName: defaultContextName } = require('../../constants')
const { codes: configurationCodes } = require('../../ConfigurationErrors')

const requiredKeys = ['client_id', 'client_secret', 'technical_account_id', 'meta_scopes', 'ims_org_id', 'private_key']

const requiredMetaScope = 'ent_cloudmgr_sdk'
const requiredKeysForJWTIntegration = ['client_id', 'client_secret', 'technical_account_id', 'meta_scopes', 'ims_org_id', 'private_key']
const requiredKeysForOAuthIntegration = ['client_id', 'client_secrets', 'technical_account_email', 'technical_account_id', 'scopes', 'ims_org_id']
const requiredMetaScopeForJWTIntegration = 'ent_cloudmgr_sdk'
const requiredScopesForOAuthIntegration = ['openid', 'AdobeID', 'read_organizations', 'additional_info.projectedProductContext', 'read_pc.dma_aem_ams']

function getContextName (options) {
if (options.Command.flags && options.Command.flags.imsContextName) {
Expand Down Expand Up @@ -46,6 +47,7 @@ module.exports = function (hookOptions) {
}

const missingKeys = []
const requiredKeys = config.oauth_enabled ? requiredKeysForOAuthIntegration : requiredKeysForJWTIntegration

requiredKeys.forEach(key => {
if (!config[key]) {
Expand All @@ -57,9 +59,16 @@ module.exports = function (hookOptions) {
throw new configurationCodes.IMS_CONTEXT_MISSING_FIELDS({ messageValues: [configKey, missingKeys.join(', ')] })
}

const metaScopes = config.meta_scopes
if (!metaScopes.includes || !metaScopes.includes(requiredMetaScope)) {
throw new configurationCodes.IMS_CONTEXT_MISSING_METASCOPE({ messageValues: [configKey, requiredMetaScope] })
if (config.oauth_enabled) {
const oauthScopes = config.scopes
if (!oauthScopes.includes || !requiredScopesForOAuthIntegration.every(scope => oauthScopes.includes(scope))) {
throw new configurationCodes.IMS_CONTEXT_MISSING_OAUTH_SCOPES({ messageValues: [configKey, requiredScopesForOAuthIntegration.join(', ')] })
}
} else {
const metaScopes = config.meta_scopes
if (!metaScopes.includes || !metaScopes.includes(requiredMetaScopeForJWTIntegration)) {
throw new configurationCodes.IMS_CONTEXT_MISSING_METASCOPE({ messageValues: [configKey, requiredMetaScopeForJWTIntegration] })
}
}
}

Expand Down
40 changes: 38 additions & 2 deletions test/hooks/prerun/check-ims-context-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ test('hook -- command from other plugin', async () => {
})).not.toThrowError()
})

test('hook -- ok', async () => {
test('hook -- ok with JWT', async () => {
setStore({
'ims.contexts.aio-cli-plugin-cloudmanager': {
client_id: 'test-client-id',
Expand All @@ -67,6 +67,27 @@ test('hook -- ok', async () => {
expect(invoke()).not.toThrowError()
})

test('hook -- ok with OAuth', async () => {
setStore({
'ims.contexts.aio-cli-plugin-cloudmanager': {
client_id: 'test-client-id',
client_secrets: ['5678'],
ims_org_id: 'someorg@AdobeOrg',
technical_account_id: '4321@techacct.adobe.com',
technical_account_email: 'unused',
scopes: [
'openid',
'AdobeID',
'read_organizations',
'additional_info.projectedProductContext',
'read_pc.dma_aem_ams',
],
oauth_enabled: true,
},
})
expect(invoke()).not.toThrowError()
})

test('hook -- fully configured cli auth enables cli auth mode', async () => {
setStore({
'ims.contexts.cli': {
Expand Down Expand Up @@ -272,7 +293,7 @@ test('hook -- missing some fields', async () => {
expect(invoke()).toThrowError('One or more of the required fields in ims.contexts.aio-cli-plugin-cloudmanager were not set. Missing keys were technical_account_id, meta_scopes, private_key.')
})

test('hook -- missing scope', async () => {
test('hook -- missing metascope for JWT', async () => {
setStore({
'ims.contexts.aio-cli-plugin-cloudmanager': {
client_id: 'test-client-id',
Expand All @@ -289,6 +310,21 @@ test('hook -- missing scope', async () => {
expect(invoke()).toThrowError('The configuration ims.contexts.aio-cli-plugin-cloudmanager is missing the required metascope ent_cloudmgr_sdk.')
})

test('hook -- missing scope for OAuth', async () => {
setStore({
'ims.contexts.aio-cli-plugin-cloudmanager': {
client_id: 'test-client-id',
client_secrets: ['5678'],
ims_org_id: 'someorg@AdobeOrg',
technical_account_id: '4321@techacct.adobe.com',
technical_account_email: 'unused',
scopes: [],
oauth_enabled: true,
},
})
expect(invoke()).toThrowError('[CloudManagerCLI:IMS_CONTEXT_MISSING_OAUTH_SCOPES] The configuration ims.contexts.aio-cli-plugin-cloudmanager is missing the required OAuth scopes openid, AdobeID, read_organizations, additional_info.projectedProductContext, read_pc.dma_aem_ams')
})

test('hook -- scope is a number', async () => {
setStore({
'ims.contexts.aio-cli-plugin-cloudmanager': {
Expand Down

0 comments on commit 0ac4a27

Please sign in to comment.