Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/api/src/auth/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/**
* credentials for app authentication
*/
export type Credentials = ClientCredentials | TokenCredentials;
export type Credentials = ClientCredentials | TokenCredentials | UserManagedIdentityCredentials;

/**
* credentials for authentication
* of an app via `clientId` and `clientSecret`
*/
export type ClientCredentials = {
type: 'clientSecret';
readonly clientId: string;
readonly clientSecret: string;
readonly tenantId?: string;
Expand All @@ -18,7 +19,17 @@ export type ClientCredentials = {
* of an app via any external auth method
*/
export type TokenCredentials = {
type: 'token';
readonly clientId: string;
readonly tenantId?: string;
readonly token: (scope: string | string[], tenantId?: string) => string | Promise<string>;
};

/**
* credentials for user managed identity
*/
export type UserManagedIdentityCredentials = {
type: 'userManagedIdentity';
readonly clientId: string;
readonly tenantId?: string;
};
6 changes: 6 additions & 0 deletions packages/api/src/clients/bot/token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('BotTokenClient', () => {
const spy = jest.spyOn(http, 'post').mockResolvedValueOnce({});

await client.get({
type: 'clientSecret',
clientId: 'test',
clientSecret: '123',
});
Expand All @@ -25,6 +26,7 @@ describe('BotTokenClient', () => {
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});

await client.get({
type: 'clientSecret',
clientId: 'test',
clientSecret: '123',
});
Expand All @@ -42,6 +44,7 @@ describe('BotTokenClient', () => {
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});

await client.get({
type: 'clientSecret',
clientId: 'test',
clientSecret: '123',
});
Expand All @@ -58,6 +61,7 @@ describe('BotTokenClient', () => {
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});

await client.get({
type: 'clientSecret',
clientId: 'test',
clientSecret: '123',
tenantId: 'test-tenant',
Expand All @@ -77,6 +81,7 @@ describe('BotTokenClient', () => {
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});

await client.getGraph({
type: 'clientSecret',
clientId: 'test',
clientSecret: '123',
});
Expand All @@ -93,6 +98,7 @@ describe('BotTokenClient', () => {
const spy = jest.spyOn(client.http, 'post').mockResolvedValueOnce({});

await client.getGraph({
type: 'clientSecret',
clientId: 'test',
clientSecret: '123',
tenantId: 'test-tenant',
Expand Down
8 changes: 8 additions & 0 deletions packages/api/src/clients/bot/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export class BotTokenClient {
};
}

if (!('clientSecret' in credentials)) {
throw new Error('Bot Token Client only supports auth via secrets');
}

const tenantId = credentials.tenantId || 'botframework.com';
const res = await this.http.post<GetBotTokenResponse>(
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
Expand Down Expand Up @@ -75,6 +79,10 @@ export class BotTokenClient {
};
}

if (!('clientSecret' in credentials)) {
throw new Error('Bot Token Client only supports auth via secrets');
}

const tenantId = credentials.tenantId || 'botframework.com';
const res = await this.http.post<GetBotTokenResponse>(
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
Expand Down
151 changes: 151 additions & 0 deletions packages/apps/src/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class TestApp extends App {
public async testGetAppGraphToken(tenantId?: string) {
return this.getAppGraphToken(tenantId);
}

public getCredentials() {
return this.credentials;
}
}

describe('App', () => {
Expand Down Expand Up @@ -180,4 +184,151 @@ describe('App', () => {
expect(mockAcquireToken).not.toHaveBeenCalled();
});
});

describe('Credentials', () => {
const originalEnv = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
});

it('should create ClientCredentials with clientSecret from options or env', () => {
// From options
const appWithSecret = new TestApp({
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
tenantId: 'test-tenant-id',
plugins: [new TestHttpPlugin()],
});

expect(appWithSecret.getCredentials()).toEqual({
type: 'clientSecret',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
tenantId: 'test-tenant-id',
});

// From environment variables
process.env.CLIENT_ID = 'env-client-id';
process.env.CLIENT_SECRET = 'env-client-secret';
process.env.TENANT_ID = 'env-tenant-id';

const appFromEnv = new TestApp({
plugins: [new TestHttpPlugin()],
});

expect(appFromEnv.getCredentials()).toEqual({
type: 'clientSecret',
clientId: 'env-client-id',
clientSecret: 'env-client-secret',
tenantId: 'env-tenant-id',
});
});

it('should create TokenCredentials with token provider', () => {
const tokenProvider = jest.fn().mockResolvedValue('mock-token');
const appWithToken = new TestApp({
clientId: 'test-client-id',
token: tokenProvider,
tenantId: 'test-tenant-id',
plugins: [new TestHttpPlugin()],
});

expect(appWithToken.getCredentials()).toEqual({
type: 'token',
clientId: 'test-client-id',
token: tokenProvider,
tenantId: 'test-tenant-id',
});
});

it('should create UserManagedIdentity credentials with only clientId from options or env', () => {
// From options
const appWithUMI = new TestApp({
clientId: 'test-client-id',
tenantId: 'test-tenant-id',
plugins: [new TestHttpPlugin()],
});

expect(appWithUMI.getCredentials()).toEqual({
type: 'userManagedIdentity',
clientId: 'test-client-id',
tenantId: 'test-tenant-id',
});

// From environment variables
process.env.CLIENT_ID = 'env-client-id';
process.env.TENANT_ID = 'env-tenant-id';

const appFromEnv = new TestApp({
plugins: [new TestHttpPlugin()],
});

expect(appFromEnv.getCredentials()).toEqual({
type: 'userManagedIdentity',
clientId: 'env-client-id',
tenantId: 'env-tenant-id',
});
});

it('should prioritize clientSecret over token when both provided', () => {
const tokenProvider = jest.fn().mockResolvedValue('mock-token');
const appWithBoth = new TestApp({
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
token: tokenProvider,
tenantId: 'test-tenant-id',
plugins: [new TestHttpPlugin()],
});

expect(appWithBoth.getCredentials()).toEqual({
type: 'clientSecret',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
tenantId: 'test-tenant-id',
});
});

it('should merge options and environment variables', () => {
// Options take precedence over env
process.env.CLIENT_ID = 'env-client-id';
process.env.CLIENT_SECRET = 'env-client-secret';
process.env.TENANT_ID = 'env-tenant-id';

const appWithOptions = new TestApp({
clientId: 'options-client-id',
clientSecret: 'options-client-secret',
tenantId: 'options-tenant-id',
plugins: [new TestHttpPlugin()],
});

expect(appWithOptions.getCredentials()).toEqual({
type: 'clientSecret',
clientId: 'options-client-id',
clientSecret: 'options-client-secret',
tenantId: 'options-tenant-id',
});

// Mix options and env
process.env.CLIENT_SECRET = 'env-client-secret';
process.env.TENANT_ID = 'env-tenant-id';

const appMerged = new TestApp({
clientId: 'options-client-id',
plugins: [new TestHttpPlugin()],
});

expect(appMerged.getCredentials()).toEqual({
type: 'clientSecret',
clientId: 'options-client-id',
clientSecret: 'env-client-secret',
tenantId: 'env-tenant-id',
});
});
});
});
15 changes: 12 additions & 3 deletions packages/apps/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,28 @@ export class App<TPlugin extends IPlugin = IPlugin> {
const token = 'token' in this.options ? this.options.token : undefined;

if (clientId && clientSecret) {
this.log.debug('Using Client Credentials auth');
this.credentials = {
type: 'clientSecret',
clientId,
clientSecret,
tenantId,
};
}

if (clientId && token) {
} else if (clientId && token) {
this.log.debug(('Using custom token factory auth'));
this.credentials = {
type: 'token',
clientId,
tenantId,
token,
};
} else if (clientId && !clientSecret) {
this.log.debug('Using user managed identity auth');
this.credentials = {
type: 'userManagedIdentity',
clientId,
tenantId
};
}

this.tokenManager = new TokenManager(this.credentials, this.log);
Expand Down
Loading