OfficeDev / teams-toolkit

Developer tools for building Teams apps
Other
459 stars 188 forks source link

SSO Implementation #12559

Open arundhathiMenon opened 1 week ago

arundhathiMenon commented 1 week ago

can you please guide me on this,

I have two accounts: arundhathi@one.ai and arundhathi@two.ai. On Azure, I have a subscription under the one.ai tenant, where I’ve registered both an app and a bot. However, I have a SharePoint license and a Microsoft Teams account associated with the arundhathi@two.ai account.

Currently, I’m implementing SSO (Single Sign-On) to authorize users when they log in to Microsoft Teams, but I am using the app registration credentials (client ID, secret, etc.) from the arundhathi@one.ai tenant.

Will this setup work, or is there a requirement to have a subscription under the two.ai tenant as well for proper SSO functionality? Would having the app and bot registered under two.ai be necessary for this scenario?

blackchoey commented 1 week ago

As long as you configured the Entra app registration as multi-tenant, it should work. Please share the error message if you meet any issues.

arundhathiMenon commented 1 week ago

index.ts File:

import * as restify from "restify"; require('dotenv').config(); import { CloudAdapter, ConfigurationServiceClientCredentialFactory, ConfigurationBotFrameworkAuthentication, TurnContext, MemoryStorage, ConversationState, UserState, } from "botbuilder"; import { TeamsBot } from "./teamsBot"; import config from "./config";

const credentialsFactory = new ConfigurationServiceClientCredentialFactory({ MicrosoftAppId: config.botId, MicrosoftAppPassword: config.botPassword, MicrosoftAppType: "MultiTenant", });

const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication( {}, credentialsFactory );

const adapter = new CloudAdapter(botFrameworkAuthentication);

const onTurnErrorHandler = async (context: TurnContext, error: Error) => { console.error(\n [onTurnError] unhandled error: ${error});

if (context.activity.type === "message") { await context.sendTraceActivity( "OnTurnError Trace", ${error}, "https://www.botframework.com/schemas/error", "TurnError" ); await context.sendActivity(The bot encountered unhandled error:\n ${error.message}); await context.sendActivity("To continue to run this bot, please fix the bot source code."); } };

adapter.onTurnError = onTurnErrorHandler;

const memoryStorage = new MemoryStorage();

const conversationState = new ConversationState(memoryStorage); const userState = new UserState(memoryStorage);

const bot = new TeamsBot(conversationState, userState);

const server = restify.createServer(); server.use(restify.plugins.bodyParser()); server.listen(process.env.port || process.env.PORT || 3978, () => { console.log(\nBot Started, ${server.name} listening to ${server.url}); });

server.post("/api/messages", async (req, res) => { await adapter.process(req, res, async (context) => { await bot.run(context); }); });

..................................... teamsBot.ts File

import { TeamsActivityHandler, CardFactory, MessageFactory, UserTokenClient, ConversationState, UserState, TurnContext, } from "botbuilder"; import OnedriveLinkInputAdaptiveCard from "./adaptiveCards/cd_one_drive_input.json"; import HelpAdaptiveCard from "./adaptiveCards/cd_help_user.json"; import GreetAdaptiveCard from "./adaptiveCards/cd_greet_user.json"; import { DialogSet, DialogTurnStatus, OAuthPrompt, WaterfallDialog, WaterfallStepContext, } from "botbuilder-dialogs"; import { CloudAdapter } from "botbuilder"; const OAUTH_PROMPT_ID = "oauthPrompt"; const MAIN_DIALOG = "mainDialog"; import config from "./config";

export class TeamsBot extends TeamsActivityHandler { private oneDriveLink: string | null; private pdfContents: string[];

private conversationState: ConversationState; private dialogState: any; private dialogs: DialogSet;

constructor(conversationState: ConversationState, userState: UserState) { super(); this.oneDriveLink = null; this.pdfContents = [];

this.conversationState = conversationState;
this.dialogState = this.conversationState.createProperty("dialogState");

this.dialogs = new DialogSet(this.dialogState);

this.dialogs.add(
  new OAuthPrompt(OAUTH_PROMPT_ID, {
    connectionName: config.ConnectionName, // Replace with your connection name
    text: "Please sign in to continue.",
    title: "Sign in",
    timeout: 300000,
  })
);

this.dialogs.add(
  new WaterfallDialog(MAIN_DIALOG, [
    this.promptForLoginStep.bind(this),
    this.processLoginResultStep.bind(this),
  ])
);
this.onMessage(async (context, next) => {
  console.log("Messages from teams channel: ", context.activity);
  const dialogContext = await this.dialogs.createContext(context);
  const results = await dialogContext.continueDialog();

  if (results.status === DialogTurnStatus.empty) {
    await dialogContext.beginDialog(MAIN_DIALOG);
  }

  await this.conversationState.saveChanges(context, false);

  await next();
});

this.onMembersAdded(async (context, next) => {
  const welcomeText =
    "Hello and welcome by tune's intelligent AI assistant";
  await context.sendActivity(MessageFactory.text(welcomeText));
  await next();
});

}

private async promptForLoginStep(step: WaterfallStepContext) { const userTokenClient: UserTokenClient = step.context.turnState.get( step.context.adapter.UserTokenClientKey ) as UserTokenClient;

const tokenResponse = await userTokenClient.getUserToken(
  step.context,
  config.ConnectionName,
  null
);
console.log("tokenResponse..", tokenResponse);
if (!tokenResponse) {
  return await step.prompt(OAUTH_PROMPT_ID, {});
} else {
  return await step.next(tokenResponse);
}

}

private async processLoginResultStep(step: WaterfallStepContext) { const tokenResponse = step.result;

if (tokenResponse && tokenResponse.token) {
  const accessToken = tokenResponse.token;
  console.log("accessToken..", accessToken);
  await step.context.sendActivity("Login successful!");
} else {
  await step.context.sendActivity("Login failed. Please try again.");
}

return await step.endDialog();

}

} ............................................ I am currently testing a Teams bot application using the BOTID and BOTPASSWORD provided by the bot service. I have configured the Azure setup according to the documentation.

I’m facing an issue with the package I am using. It keeps returning errors indicating that certain functionality is not present in the package. I believe the botbuilder package might have been updated, and I am looking for the correct package that supports Single Sign-On (SSO) and allows retrieval of an access token with user context.

Could you please provide guidance on the latest package version I should use to enable SSO and obtain an access token with the user's context in a Teams bot application?

blackchoey commented 6 days ago

You could refer this sample for implementation details: https://github.com/OfficeDev/Microsoft-Teams-Samples/tree/main/samples/bot-conversation-sso-quickstart/js

arundhathiMenon commented 3 days ago

i am refering to https://github.com/OfficeDev/Microsoft-Teams-Samples/blob/main/samples/bot-sso-adaptivecard/nodejs/bots/botSSOAdaptiveCard.js, here i am getting error : [onTurnError] unhandled error: TypeError: context.adapter.getSignInLink is not a function

Don't we have getSignInLink method on the adapter object in the current version?

blackchoey commented 2 days ago

@arundhathiMenon To get better support, could you please create an issue in the Microsoft-Teams-Samples repo asking them to update the sample code? You could also create an issue in the botbuilder package's repo for help: https://github.com/microsoft/botbuilder-js.