// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Text.Json; using Microsoft.Teams.Api; using Microsoft.Teams.Api.Activities; using Microsoft.Teams.Api.Clients; namespace Microsoft.Teams.Apps; public partial interface IContext { /// /// is the activity sender signed in? /// public bool IsSignedIn { get; set; } /// /// the default connection name to use for the app. /// by default it is "graph". /// public string ConnectionName { get; set; } /// /// trigger user OAuth signin flow for the activity sender /// /// option overrides /// optional cancellation token /// the existing user token if found public Task SignIn(OAuthOptions? options = null, CancellationToken cancellationToken = default); /// /// trigger user SSO signin flow for the activity sender /// /// option overrides /// optional cancellation token public Task SignIn(SSOOptions options, CancellationToken cancellationToken = default); /// /// trigger user signin flow for the activity sender /// /// the connection name /// optional cancellation token public Task SignOut(string? connectionName = null, CancellationToken cancellationToken = default); } public partial class Context : IContext { public bool IsSignedIn { get; set; } = false; public required string ConnectionName { get; set; } public async Task SignIn(OAuthOptions? options = null, CancellationToken cancellationToken = default) { options ??= new OAuthOptions(); var reference = Ref.Copy(); var token = cancellationToken == default ? CancellationToken : cancellationToken; var api = new ApiClient(Api, token); try { var tokenResponse = await api.Users.Token.GetAsync(new() { UserId = Activity.From.Id, ChannelId = Activity.ChannelId, ConnectionName = options.ConnectionName ?? ConnectionName, }).ConfigureAwait(false); return tokenResponse.Token; } catch (Exception ex) { Log.Debug("Existing token retrieval failed, proceeding to token exchange", ex); } var tokenExchangeState = new Api.TokenExchange.State() { ConnectionName = options.ConnectionName ?? ConnectionName, Conversation = reference, MsAppId = AppId }; if (Activity.Conversation.IsGroup == true) { // create new 1:1 conversation with user to do SSO // because groupchats don't support it. var (id, _, _) = await api.Conversations.CreateAsync(new() { TenantId = Ref.Conversation.TenantId, Members = [Activity.From] }).ConfigureAwait(false); reference.Conversation.Id = id; reference.Conversation.IsGroup = false; var oauthCardActivity = await Sender.Send(new MessageActivity(options.OAuthCardText), reference, token).ConfigureAwait(false); await OnActivitySent(oauthCardActivity, ToActivityType()).ConfigureAwait(false); } var state = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(tokenExchangeState)); var resource = await api.Bots.SignIn.GetResourceAsync(new() { State = state }).ConfigureAwait(false); var activity = new MessageActivity(); activity.Recipient = Activity.From; activity.Conversation = reference.Conversation; activity.AddAttachment(new Api.Cards.OAuthCard() { Text = options.OAuthCardText, ConnectionName = options.ConnectionName ?? ConnectionName, TokenExchangeResource = resource.TokenExchangeResource, TokenPostResource = resource.TokenPostResource, Buttons = [ new(Teams.Api.Cards.ActionType.SignIn) { Title = options.SignInButtonText, Value = resource.SignInLink } ] }); var res = await Sender.Send(activity, reference, token).ConfigureAwait(false); await OnActivitySent(res, ToActivityType()).ConfigureAwait(false); return null; } public async Task SignIn(SSOOptions options, CancellationToken cancellationToken = default) { var token = cancellationToken == default ? CancellationToken : cancellationToken; var signInLink = $"{options.SignInLink}?scope={Uri.EscapeDataString(string.Join(" ", options.Scopes))}&clientId={AppId}&tenantId={TenantId}"; var reference = Ref.Copy(); if (Activity.Conversation.IsGroup == true) { // create new 1:1 conversation with user to do SSO // because groupchats don't support it. var (id, _, _) = await Api.Conversations.CreateAsync(new() { TenantId = Ref.Conversation.TenantId, Members = [Activity.From] }).ConfigureAwait(false); reference.Conversation.Id = id; reference.Conversation.IsGroup = false; var oauthCardActivity = await Sender.Send(new MessageActivity(options.OAuthCardText), reference, token).ConfigureAwait(false); await OnActivitySent(oauthCardActivity, ToActivityType()).ConfigureAwait(false); } var activity = new MessageActivity(); activity.Recipient = Activity.From; activity.Conversation = reference.Conversation; activity.AddAttachment(new Api.Cards.OAuthCard() { Text = options.OAuthCardText, TokenExchangeResource = new() { Id = Guid.NewGuid().ToString() }, Buttons = [ new(Teams.Api.Cards.ActionType.SignIn) { Title = options.SignInButtonText, Value = options.SignInLink } ] }); var res = await Sender.Send(activity, reference, token).ConfigureAwait(false); await OnActivitySent(res, ToActivityType()).ConfigureAwait(false); } public async Task SignOut(string? connectionName = null, CancellationToken cancellationToken = default) { var token = cancellationToken == default ? CancellationToken : cancellationToken; var api = new ApiClient(Api, token); await api.Users.Token.SignOutAsync(new() { ChannelId = Ref.ChannelId, UserId = Activity.From.Id, ConnectionName = connectionName ?? ConnectionName, }).ConfigureAwait(false); } } /// /// base sign in options type /// public abstract class SignInOptions { /// /// the oauth card text /// public string OAuthCardText { get; set; } = "Please Sign In..."; /// /// the sign in button text /// public string SignInButtonText { get; set; } = "Sign In"; } /// /// OAuth sign in options /// public class OAuthOptions : SignInOptions { /// /// the auth connection name to use, defaults /// to the default connection name of the app /// public string? ConnectionName { get; set; } } /// /// SSO sign in options /// public class SSOOptions : SignInOptions { /// /// the scopes to request consent for /// public required string[] Scopes { get; set; } /// /// the sign in link to use, defaults to /// the link returned by the sign in resource /// public required string SignInLink { get; set; } }