Skip to content
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,25 @@ const { generateAccessToken } = require('@adobe/aio-lib-core-auth')

async function main(params) {
try {
// Note: Will cache for 5 min
// if the include-ims-credentials annotation is set, the library infers credentials from the Runtime params
const token = await generateAccessToken(params)

// otherwise credentials can be passed manually
const token = await generateAccessToken({
clientId: params.IMS_CLIENT_ID,
clientSecret: params.IMS_CLIENT_SECRET,
orgId: params.IMS_ORG_ID,
scopes: params.IMS_SCOPES,
clientId,
clientSecret,
orgId,
scopes
})

console.log('Authentication successful:', token.access_token)
} catch (error) {
console.error('Authentication failed:', error)
}
}
```

Note: The token is cached in the Runtime's container memory, a single Runtime action can run in multiple containers.
Note: The token is cached for 5 minutes in the Runtime's container memory. A single Runtime action can run in multiple containers, meaning the cache is not shared across actions.

### Invalidating the Token Cache in a Runtime action

Expand Down
14 changes: 14 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Copyright 2026 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

// support parameters set in the include-ims-credentials annotation
export const IMS_OAUTH_S2S_INPUT = '__ims_oauth_s2s'
export const IMS_ENV_INPUT = '__ims_env'
2 changes: 1 addition & 1 deletion src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const E = ErrorWrapper(

// Error codes
E('IMS_TOKEN_ERROR', 'Error calling IMS to get access token: %s')
E('MISSING_PARAMETERS', 'Missing required parameters: %s')
E('MISSING_PARAMETERS', 'Missing required parameters: %s. You may want to set the include-ims-credentials annotation.')
E('BAD_CREDENTIALS_FORMAT', 'Credentials must be either an object or a stringified object')
E('BAD_SCOPES_FORMAT', 'Scopes must be an array')
E('GENERIC_ERROR', 'An unexpected error occurred: %s')
Expand Down
25 changes: 14 additions & 11 deletions src/ims.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,23 @@ function getImsUrl (env) {
*
* @private
* @param {object} params - Parameters to validate
* @returns {object} Validated credentials object
* @throws {Error} If any required parameters are missing
* @returns {{ error, credentials }} Object with error (if any) and validated credentials object
*/
export function getAndValidateCredentials (params) {
if (!(typeof params === 'object' && params !== null && !Array.isArray(params))) {
throw new codes.BAD_CREDENTIALS_FORMAT({
sdkDetails: { paramsType: typeof params }
})
return {
error: new codes.BAD_CREDENTIALS_FORMAT({
sdkDetails: { paramsType: typeof params }
})
}
}

if (params.scopes && !Array.isArray(params.scopes)) {
throw new codes.BAD_SCOPES_FORMAT({
sdkDetails: { scopesType: typeof params.scopes }
})
return {
error: new codes.BAD_SCOPES_FORMAT({
sdkDetails: { scopesType: typeof params.scopes }
})
}
}

const credentials = {}
Expand All @@ -68,13 +71,13 @@ export function getAndValidateCredentials (params) {
}

if (missingParams.length > 0) {
throw new codes.MISSING_PARAMETERS({
return { error: new codes.MISSING_PARAMETERS({
messageValues: missingParams.join(', '),
sdkDetails: { clientId, orgId, scopes }
})
}) }
}

return credentials
return { credentials, error: null }
}

/**
Expand Down
17 changes: 15 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ governing permissions and limitations under the License.
import { getAccessTokenByClientCredentials, getAndValidateCredentials } from './ims.js'
import { TTLCache } from '@isaacs/ttlcache'
import crypto from 'crypto'
import { IMS_ENV_INPUT, IMS_OAUTH_S2S_INPUT } from './constants.js'

// Token cache with TTL
// Opinionated for now, we could make it configurable in the future if needed -mg
Expand Down Expand Up @@ -55,9 +56,21 @@ export function invalidateCache () {
* @throws {Error} If there's an error getting the access token
*/
export async function generateAccessToken (params, imsEnv) {
imsEnv = imsEnv || (ioRuntimeStageNamespace() ? 'stage' : 'prod')
// integrate with the runtime environment and include-ims-credentials annotation
imsEnv = imsEnv || params?.[IMS_ENV_INPUT] || (ioRuntimeStageNamespace() ? 'stage' : 'prod')

const credentials = getAndValidateCredentials(params)
let credentials

// get parameters from params in priority otherwise try to load the credentials set to params.__ims_oauth_s2s by the annotation
const fromParams = getAndValidateCredentials(params)
credentials = fromParams.credentials
if (fromParams.error) {
const fromAnnotation = getAndValidateCredentials(params?.[IMS_OAUTH_S2S_INPUT])
if (fromAnnotation.error) {
throw fromParams.error // still throw original error
}
credentials = fromAnnotation.credentials
}

const credAndEnv = { ...credentials, env: imsEnv }

Expand Down
145 changes: 76 additions & 69 deletions test/ims.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,8 @@ describe('getAndValidateCredentials', () => {

const result = getAndValidateCredentials(params)

expect(result).toEqual({
expect(result.error).toBeNull()
expect(result.credentials).toEqual({
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
orgId: 'test-org-id',
Expand All @@ -392,7 +393,8 @@ describe('getAndValidateCredentials', () => {

const result = getAndValidateCredentials(params)

expect(result).toEqual({
expect(result.error).toBeNull()
expect(result.credentials).toEqual({
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
orgId: 'test-org-id',
Expand All @@ -412,9 +414,10 @@ describe('getAndValidateCredentials', () => {

const result = getAndValidateCredentials(params)

expect(result.clientId).toBe('camel-client-id')
expect(result.clientSecret).toBe('camel-secret')
expect(result.orgId).toBe('camel-org-id')
expect(result.error).toBeNull()
expect(result.credentials.clientId).toBe('camel-client-id')
expect(result.credentials.clientSecret).toBe('camel-secret')
expect(result.credentials.orgId).toBe('camel-org-id')
})

test('defaults scopes to empty array when not provided', () => {
Expand All @@ -426,132 +429,135 @@ describe('getAndValidateCredentials', () => {

const result = getAndValidateCredentials(params)

expect(result.scopes).toEqual([])
expect(result.error).toBeNull()
expect(result.credentials.scopes).toEqual([])
})

test('throws BAD_CREDENTIALS_FORMAT when params is null', () => {
expect(() => getAndValidateCredentials(null))
.toThrow(codes.BAD_CREDENTIALS_FORMAT)
test('returns BAD_CREDENTIALS_FORMAT error when params is null', () => {
const result = getAndValidateCredentials(null)

let error
try {
getAndValidateCredentials(null)
} catch (e) {
error = e
}
expect(error.name).toBe('AuthSDKError')
expect(error.code).toBe('BAD_CREDENTIALS_FORMAT')
expect(result.error).toBeDefined()
expect(result.error.name).toBe('AuthSDKError')
expect(result.error.code).toBe('BAD_CREDENTIALS_FORMAT')
})

test('throws BAD_CREDENTIALS_FORMAT when params is undefined', () => {
expect(() => getAndValidateCredentials(undefined))
.toThrow(codes.BAD_CREDENTIALS_FORMAT)
test('returns BAD_CREDENTIALS_FORMAT error when params is undefined', () => {
const result = getAndValidateCredentials(undefined)

expect(result.error).toBeDefined()
expect(result.error.code).toBe('BAD_CREDENTIALS_FORMAT')
})

test('throws BAD_CREDENTIALS_FORMAT when params is an array', () => {
expect(() => getAndValidateCredentials(['test']))
.toThrow(codes.BAD_CREDENTIALS_FORMAT)
test('returns BAD_CREDENTIALS_FORMAT error when params is an array', () => {
const result = getAndValidateCredentials(['test'])

expect(result.error).toBeDefined()
expect(result.error.code).toBe('BAD_CREDENTIALS_FORMAT')
})

test('throws BAD_CREDENTIALS_FORMAT when params is a string', () => {
expect(() => getAndValidateCredentials('test'))
.toThrow(codes.BAD_CREDENTIALS_FORMAT)
test('returns BAD_CREDENTIALS_FORMAT error when params is a string', () => {
const result = getAndValidateCredentials('test')

expect(result.error).toBeDefined()
expect(result.error.code).toBe('BAD_CREDENTIALS_FORMAT')
})

test('throws BAD_CREDENTIALS_FORMAT when params is a number', () => {
expect(() => getAndValidateCredentials(123))
.toThrow(codes.BAD_CREDENTIALS_FORMAT)
test('returns BAD_CREDENTIALS_FORMAT error when params is a number', () => {
const result = getAndValidateCredentials(123)

expect(result.error).toBeDefined()
expect(result.error.code).toBe('BAD_CREDENTIALS_FORMAT')
})

test('throws MISSING_PARAMETERS when clientId is missing', () => {
test('returns MISSING_PARAMETERS error when clientId is missing', () => {
const params = {
clientSecret: 'test-client-secret',
orgId: 'test-org-id'
}

expect(() => getAndValidateCredentials(params))
.toThrow(codes.MISSING_PARAMETERS)
const result = getAndValidateCredentials(params)

expect(result.error).toBeDefined()
expect(result.error.code).toBe('MISSING_PARAMETERS')
})

test('throws MISSING_PARAMETERS when clientSecret is missing', () => {
test('returns MISSING_PARAMETERS error when clientSecret is missing', () => {
const params = {
clientId: 'test-client-id',
orgId: 'test-org-id'
}

expect(() => getAndValidateCredentials(params))
.toThrow(codes.MISSING_PARAMETERS)
const result = getAndValidateCredentials(params)

expect(result.error).toBeDefined()
expect(result.error.code).toBe('MISSING_PARAMETERS')
})

test('throws MISSING_PARAMETERS when orgId is missing', () => {
test('returns MISSING_PARAMETERS error when orgId is missing', () => {
const params = {
clientId: 'test-client-id',
clientSecret: 'test-client-secret'
}

expect(() => getAndValidateCredentials(params))
.toThrow(codes.MISSING_PARAMETERS)
const result = getAndValidateCredentials(params)

expect(result.error).toBeDefined()
expect(result.error.code).toBe('MISSING_PARAMETERS')
})

test('throws MISSING_PARAMETERS with all missing params listed', () => {
let error
try {
getAndValidateCredentials({})
} catch (e) {
error = e
}
test('returns MISSING_PARAMETERS with all missing params listed', () => {
const result = getAndValidateCredentials({})

expect(error.name).toBe('AuthSDKError')
expect(error.code).toBe('MISSING_PARAMETERS')
expect(error.message).toContain('clientId')
expect(error.message).toContain('clientSecret')
expect(error.message).toContain('orgId')
expect(result.error).toBeDefined()
expect(result.error.name).toBe('AuthSDKError')
expect(result.error.code).toBe('MISSING_PARAMETERS')
expect(result.error.message).toContain('clientId')
expect(result.error.message).toContain('clientSecret')
expect(result.error.message).toContain('orgId')
})

test('throws BAD_SCOPES_FORMAT when scopes is a string', () => {
test('returns BAD_SCOPES_FORMAT error when scopes is a string', () => {
const params = {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
orgId: 'test-org-id',
scopes: 'openid'
}

expect(() => getAndValidateCredentials(params))
.toThrow(codes.BAD_SCOPES_FORMAT)
const result = getAndValidateCredentials(params)

let error
try {
getAndValidateCredentials(params)
} catch (e) {
error = e
}
expect(error.name).toBe('AuthSDKError')
expect(error.code).toBe('BAD_SCOPES_FORMAT')
expect(error.sdkDetails.scopesType).toBe('string')
expect(result.error).toBeDefined()
expect(result.error.name).toBe('AuthSDKError')
expect(result.error.code).toBe('BAD_SCOPES_FORMAT')
expect(result.error.sdkDetails.scopesType).toBe('string')
})

test('throws BAD_SCOPES_FORMAT when scopes is an object', () => {
test('returns BAD_SCOPES_FORMAT error when scopes is an object', () => {
const params = {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
orgId: 'test-org-id',
scopes: { scope: 'openid' }
}

expect(() => getAndValidateCredentials(params))
.toThrow(codes.BAD_SCOPES_FORMAT)
const result = getAndValidateCredentials(params)

expect(result.error).toBeDefined()
expect(result.error.code).toBe('BAD_SCOPES_FORMAT')
})

test('throws BAD_SCOPES_FORMAT when scopes is a number', () => {
test('returns BAD_SCOPES_FORMAT error when scopes is a number', () => {
const params = {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
orgId: 'test-org-id',
scopes: 123
}

expect(() => getAndValidateCredentials(params))
.toThrow(codes.BAD_SCOPES_FORMAT)
const result = getAndValidateCredentials(params)

expect(result.error).toBeDefined()
expect(result.error.code).toBe('BAD_SCOPES_FORMAT')
})

test('accepts scopes as an array', () => {
Expand All @@ -563,6 +569,7 @@ describe('getAndValidateCredentials', () => {
}

const result = getAndValidateCredentials(params)
expect(result.scopes).toEqual(['openid', 'profile'])
expect(result.error).toBeNull()
expect(result.credentials.scopes).toEqual(['openid', 'profile'])
})
})
Loading