• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 386740

18 Mar 2024 02:55PM UTC coverage: 78.215% (-0.03%) from 78.243%
386740

push

CI-PR build

web-flow
Move SaveAllChanges method from SetProperty to OAuthInput (#6757)

26188 of 33482 relevant lines covered (78.22%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.21
/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Input/OAuthInput.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Generic;
6
using System.Globalization;
7
using System.Threading;
8
using System.Threading.Tasks;
9
using AdaptiveExpressions.Properties;
10
using Microsoft.Bot.Builder.Dialogs.Adaptive.Conditions;
11
using Microsoft.Bot.Builder.Dialogs.Memory;
12
using Microsoft.Bot.Schema;
13
using Newtonsoft.Json;
14

15
namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Input
16
{
17
    /// <summary>
18
    /// OAuthInput prompts user to login.
19
    /// </summary>
20
    public class OAuthInput : InputDialog
21
    {
22
        /// <summary>
23
        /// Class identifier.
24
        /// </summary>
25
        [JsonProperty("$kind")]
26
        public const string Kind = "Microsoft.OAuthInput";
27

28
        private const string PersistedOptions = "options";
29
        private const string PersistedState = "state";
30
        private const string PersistedExpires = "expires";
31
        private const string AttemptCountKey = "AttemptCount";
32

33
        /// <summary>
34
        /// Gets or sets the name of the OAuth connection.
35
        /// </summary>
36
        /// <value>String or expression which evaluates to a string.</value>
37
        [JsonProperty("connectionName")]
38
        public StringExpression ConnectionName { get; set; }
1✔
39

40
        /// <summary>
41
        /// Gets or sets the title of the sign-in card.
42
        /// </summary>
43
        /// <value>String or expression which evaluates to string.</value>
44
        [JsonProperty("title")]
45
        public StringExpression Title { get; set; }
1✔
46

47
        /// <summary>
48
        /// Gets or sets any additional text to include in the sign-in card.
49
        /// </summary>
50
        /// <value>String or expression which evaluates to a string.</value>
51
        [JsonProperty("text")]
52
        public StringExpression Text { get; set; }
1✔
53

54
        /// <summary>
55
        /// Gets or sets the number of milliseconds the prompt waits for the user to authenticate.
56
        /// Default is 900,000 (15 minutes).
57
        /// </summary>
58
        /// <value>Int or expression which evaluates to int.</value>
59
        [JsonProperty("timeout")]
60
        public IntExpression Timeout { get; set; } = 900000;
1✔
61

62
        /// <summary>
63
        /// Called when a prompt dialog is pushed onto the dialog stack and is being activated.
64
        /// </summary>
65
        /// <param name="dc">The dialog context for the current turn of the conversation.</param>
66
        /// <param name="options">Optional, additional information to pass to the prompt being started.</param>
67
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
68
        /// or threads to receive notice of cancellation.</param>
69
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
70
        /// <remarks>If the task is successful, the result indicates whether the prompt is still
71
        /// active after the turn has been processed by the prompt.</remarks>
72
        public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
73
        {
74
            if (dc == null)
1✔
75
            {
76
                throw new ArgumentNullException(nameof(dc));
×
77
            }
78

79
            if (options is CancellationToken)
1✔
80
            {
81
                throw new ArgumentException($"{nameof(options)} cannot be a cancellation token");
×
82
            }
83

84
            if (Disabled != null && Disabled.GetValue(dc.State))
×
85
            {
86
                return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
×
87
            }
88

89
            PromptOptions opt = null;
1✔
90
            if (options != null)
1✔
91
            {
92
                if (options is PromptOptions)
×
93
                {
94
                    // Ensure prompts have input hint set
95
                    opt = options as PromptOptions;
×
96
                    if (opt.Prompt != null && string.IsNullOrEmpty(opt.Prompt.InputHint))
×
97
                    {
98
                        opt.Prompt.InputHint = InputHints.AcceptingInput;
×
99
                    }
100

101
                    if (opt.RetryPrompt != null && string.IsNullOrEmpty(opt.RetryPrompt.InputHint))
×
102
                    {
103
                        opt.RetryPrompt.InputHint = InputHints.AcceptingInput;
×
104
                    }
105
                }
106
            }
107

108
            var op = OnInitializeOptions(dc, options);
1✔
109
            dc.State.SetValue(ThisPath.Options, op);
1✔
110
            dc.State.SetValue(TURN_COUNT_PROPERTY, 0);
1✔
111

112
            // If AlwaysPrompt is set to true, then clear Property value for turn 0.
113
            if (this.Property != null && this.AlwaysPrompt != null && this.AlwaysPrompt.GetValue(dc.State))
×
114
            {
115
                dc.State.SetValue(this.Property.GetValue(dc.State), null);
×
116
            }
117

118
            // Initialize state
119
            var state = dc.ActiveDialog.State;
1✔
120
            state[PersistedOptions] = opt;
1✔
121
            state[PersistedState] = new Dictionary<string, object>
1✔
122
            {
1✔
123
                { AttemptCountKey, 0 },
1✔
124
            };
1✔
125

126
            state[PersistedExpires] = DateTime.UtcNow.AddMilliseconds(Timeout.GetValue(dc.State));
1✔
127
            OAuthPrompt.SetCallerInfoInDialogState(state, dc.Context);
1✔
128

129
            // Attempt to get the users token
130
            var output = await GetUserTokenAsync(dc, cancellationToken).ConfigureAwait(false);
1✔
131
            if (output != null)
1✔
132
            {
133
                if (this.Property != null)
1✔
134
                {
135
                    dc.State.SetValue(this.Property.GetValue(dc.State), output);
1✔
136
                }
137

138
                // Return token
139
                return await dc.EndDialogAsync(output, cancellationToken).ConfigureAwait(false);
1✔
140
            }
141
            else
142
            {
143
                dc.State.SetValue(TURN_COUNT_PROPERTY, 1);
1✔
144

145
                // Prompt user to login
146
                await SendOAuthCardAsync(dc, opt?.Prompt, cancellationToken).ConfigureAwait(false);
×
147
                return Dialog.EndOfTurn;
1✔
148
            }
149
        }
1✔
150

151
        /// <summary>
152
        /// Called when a prompt dialog is the active dialog and the user replied with a new activity.
153
        /// </summary>
154
        /// <param name="dc">The dialog context for the current turn of conversation.</param>
155
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
156
        /// or threads to receive notice of cancellation.</param>
157
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
158
        /// <remarks>If the task is successful, the result indicates whether the dialog is still
159
        /// active after the turn has been processed by the dialog.
160
        /// <para>The prompt generally continues to receive the user's replies until it accepts the
161
        /// user's reply as valid input for the prompt.</para></remarks>
162
        public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
163
        {
164
            if (dc == null)
1✔
165
            {
166
                throw new ArgumentNullException(nameof(dc));
×
167
            }
168

169
            var interrupted = dc.State.GetValue<bool>(TurnPath.Interrupted, () => false);
1✔
170
            var turnCount = dc.State.GetValue<int>(TURN_COUNT_PROPERTY, () => 0);
1✔
171

172
            // Recognize token
173
            var recognized = await RecognizeTokenAsync(dc, cancellationToken).ConfigureAwait(false);
1✔
174

175
            // Check for timeout
176
            var state = dc.ActiveDialog.State;
1✔
177
            var expires = (DateTime)state[PersistedExpires];
1✔
178
            var isMessage = dc.Context.Activity.Type == ActivityTypes.Message;
1✔
179
            var isTimeoutActivityType = isMessage
×
180
                                        || IsTokenResponseEvent(dc.Context)
×
181
                                        || IsTeamsVerificationInvoke(dc.Context)
×
182
                                        || IsTokenExchangeRequestInvoke(dc.Context);
×
183
            var hasTimedOut = isTimeoutActivityType && (DateTime.Compare(DateTime.UtcNow, expires) > 0);
×
184

185
            if (hasTimedOut)
1✔
186
            {
187
                if (this.Property != null)
×
188
                {
189
                    dc.State.SetValue(this.Property.GetValue(dc.State), null);
×
190
                }
191

192
                // if the token fetch request times out, complete the prompt with no result.
193
                return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
×
194
            }
195
            else
196
            {
197
                var promptState = (IDictionary<string, object>)state[PersistedState];
1✔
198
                var promptOptions = (PromptOptions)state[PersistedOptions];
1✔
199

200
                // Increment attempt count
201
                // Convert.ToInt32 For issue https://github.com/Microsoft/botbuilder-dotnet/issues/1859
202
                promptState[AttemptCountKey] = Convert.ToInt32(promptState[AttemptCountKey], CultureInfo.InvariantCulture) + 1;
1✔
203

204
                // Validate the return value
205
                var inputState = InputState.Invalid;
1✔
206
                if (recognized.Succeeded)
1✔
207
                {
208
                    inputState = InputState.Valid;
1✔
209
                }
210

211
                // Return recognized value or re-prompt
212
                if (inputState == InputState.Valid)
1✔
213
                {
214
                    if (this.Property != null)
1✔
215
                    {
216
                        dc.State.SetValue(this.Property.GetValue(dc.State), recognized.Value);
1✔
217
                    }
218

219
                    return await dc.EndDialogAsync(recognized.Value, cancellationToken).ConfigureAwait(false);
1✔
220
                }
221
                else if (this.MaxTurnCount == null || turnCount < this.MaxTurnCount.GetValue(dc.State))
×
222
                {
223
                    if (!interrupted)
1✔
224
                    { 
225
                        // increase the turnCount as last step
226
                        dc.State.SetValue(TURN_COUNT_PROPERTY, turnCount + 1);
1✔
227

228
                        if (isMessage)
1✔
229
                        {
230
                            var prompt = await this.OnRenderPromptAsync(dc, inputState, cancellationToken).ConfigureAwait(false);
1✔
231
                            await dc.Context.SendActivityAsync(prompt, cancellationToken).ConfigureAwait(false);
1✔
232
                        }
233
                    }
234

235
                    // Only send the card in response to a message.
236
                    if (isMessage)
1✔
237
                    {
238
                        await SendOAuthCardAsync(dc, promptOptions?.Prompt, cancellationToken).ConfigureAwait(false);
×
239
                    }
240

241
                    return Dialog.EndOfTurn;
1✔
242
                }
243
                else
244
                {
245
                    if (this.DefaultValue != null)
×
246
                    {
247
                        var (value, _) = this.DefaultValue.TryGetValue(dc.State);
×
248
                        if (this.DefaultValueResponse != null)
×
249
                        {
250
                            var response = await this.DefaultValueResponse.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
×
251
                            var properties = new Dictionary<string, string>()
×
252
                            {
×
253
                                { "template", JsonConvert.SerializeObject(this.DefaultValueResponse, new JsonSerializerSettings { MaxDepth = null }) },
×
254
                                { "result", response == null ? string.Empty : JsonConvert.SerializeObject(response, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, MaxDepth = null }) },
×
255
                                { "context", TelemetryLoggerConstants.OAuthInputResultEvent }
×
256
                            };
×
257
                            TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
×
258
                            await dc.Context.SendActivityAsync(response, cancellationToken).ConfigureAwait(false);
×
259
                        }
260

261
                        // set output property
262
                        dc.State.SetValue(this.Property.GetValue(dc.State), value);
×
263
                        return await dc.EndDialogAsync(value, cancellationToken).ConfigureAwait(false);
×
264
                    }
265
                }
266

267
                return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
×
268
            }
269
        }
1✔
270

271
        /// <summary>
272
        /// Attempts to get the user's token.
273
        /// </summary>
274
        /// <param name="dc">DialogContext for the current turn of conversation with the user.</param>
275
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
276
        /// or threads to receive notice of cancellation.</param>
277
        /// <returns>A task that represents the work queued to execute.</returns>
278
        /// <remarks>If the task is successful and user already has a token or the user successfully signs in,
279
        /// the result contains the user's token.</remarks>
280
        public async Task<TokenResponse> GetUserTokenAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
281
        {
282
            var settings = new OAuthPromptSettings { ConnectionName = ConnectionName?.GetValue(dc.State) };
×
283
            return await new OAuthPrompt(nameof(OAuthPrompt), settings).GetUserTokenAsync(dc.Context, cancellationToken).ConfigureAwait(false);
1✔
284
        }
1✔
285

286
        /// <summary>
287
        /// Signs out the user.
288
        /// </summary>
289
        /// <param name="dc">DialogContext for the current turn of conversation with the user.</param>
290
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
291
        /// or threads to receive notice of cancellation.</param>
292
        /// <returns>A task that represents the work queued to execute.</returns>
293
        public async Task SignOutUserAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
294
        {
295
            var settings = new OAuthPromptSettings { ConnectionName = ConnectionName?.GetValue(dc.State) };
×
296
            await new OAuthPrompt(nameof(OAuthPrompt), settings).SignOutUserAsync(dc.Context, cancellationToken).ConfigureAwait(false);
×
297
        }
×
298

299
        /// <summary>
300
        /// Called when input has been received.
301
        /// </summary>
302
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
303
        /// <param name="cancellationToken">Optional, the <see cref="CancellationToken"/> that can be used by other objects or threads to receive notice of cancellation.</param>
304
        /// <returns>InputState which reflects whether input was recognized as valid or not.</returns>
305
        /// <remark>Method not implemented.</remark>
306
        protected override Task<InputState> OnRecognizeInputAsync(DialogContext dc, CancellationToken cancellationToken = default)
307
        {
308
            throw new NotImplementedException();
×
309
        }
310

311
        private async Task SendOAuthCardAsync(DialogContext dc, IMessageActivity prompt, CancellationToken cancellationToken)
312
        {
313
            // Save state prior to sending OAuthCard: the invoke response for a token exchange from the root bot could come in
314
            // before this method ends or could land in another instance in scale-out scenarios, which means that if the state is not saved, 
315
            // the OAuthInput would not be at the top of the stack, and the token exchange invoke would get discarded.
316
            await dc.Context.TurnState.Get<DialogStateManager>().SaveAllChangesAsync(cancellationToken).ConfigureAwait(false);
1✔
317

318
            // Prepare OAuthCard
319
            var title = Title == null ? null : await Title.GetValueAsync(dc, cancellationToken).ConfigureAwait(false);
×
320
            var text = Text == null ? null : await Text.GetValueAsync(dc, cancellationToken).ConfigureAwait(false);
×
321
            var settings = new OAuthPromptSettings { ConnectionName = ConnectionName?.GetValue(dc.State), Title = title, Text = text };
×
322

323
            // Send OAuthCard to root bot. The root bot could attempt to do a token exchange or if it cannot do token exchange for this connection
324
            // it will let the card get to the user to allow them to sign in.
325
            await OAuthPrompt.SendOAuthCardAsync(settings, dc.Context, prompt, cancellationToken).ConfigureAwait(false);
1✔
326
        }
1✔
327

328
        private Task<PromptRecognizerResult<TokenResponse>> RecognizeTokenAsync(DialogContext dc, CancellationToken cancellationToken)
329
        {
330
            var settings = new OAuthPromptSettings { ConnectionName = ConnectionName?.GetValue(dc.State) };
×
331
            return OAuthPrompt.RecognizeTokenAsync(settings, dc, cancellationToken);
1✔
332
        }
333

334
        private bool IsTokenResponseEvent(ITurnContext turnContext)
335
        {
336
            var activity = turnContext.Activity;
×
337
            return activity.Type == ActivityTypes.Event && activity.Name == SignInConstants.TokenResponseEventName;
×
338
        }
339

340
        private bool IsTeamsVerificationInvoke(ITurnContext turnContext)
341
        {
342
            var activity = turnContext.Activity;
×
343
            return activity.Type == ActivityTypes.Invoke && activity.Name == SignInConstants.VerifyStateOperationName;
×
344
        }
345

346
        private bool IsTokenExchangeRequestInvoke(ITurnContext turnContext)
347
        {
348
            var activity = turnContext.Activity;
×
349
            return activity.Type == ActivityTypes.Invoke && activity.Name == SignInConstants.TokenExchangeOperationName;
×
350
        }
351
    }
352
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc