-
Notifications
You must be signed in to change notification settings - Fork 0
/
feed.rss
5326 lines (5004 loc) · 509 KB
/
feed.rss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>Ian Bebbington</title>
<link>http://ian.bebbs.co.uk/</link>
<description>IObservable<Opinion></description>
<copyright>2021</copyright>
<pubDate>Mon, 12 Jul 2021 13:01:25 GMT</pubDate>
<lastBuildDate>Mon, 12 Jul 2021 13:01:25 GMT</lastBuildDate>
<item>
<title>State-Of-The-Art Natural Language Processing in .NET on the Edge</title>
<link>http://ian.bebbs.co.uk/posts/Unoonnx</link>
<description><p>In this post I show how .NET can be used to run state-of-the-art Natural Language Processing (NLP) models on "the edge". I provide a simple means for downloading and converting 'transformer' models from <a href="https://huggingface.co/">HuggingFace</a> into models that can perform inference from managed .NET code on resource constrained devices. Finally I use <a href="https://platform.uno/">Uno Platform</a> to implement a cross-platform user-interface that allows real-time inference using these models.</p></description>
<guid>http://ian.bebbs.co.uk/posts/Unoonnx</guid>
<pubDate>Thu, 06 May 2021 00:00:00 GMT</pubDate>
<content:encoded><h2 id="tldr">TL;DR</h2>
<p>In this post I show how .NET can be used to run state-of-the-art Natural Language Processing (NLP) models on &quot;the edge&quot;. I provide a simple means for downloading and converting 'transformer' models from <a href="https://huggingface.co/">HuggingFace</a> into models that can perform inference from managed .NET code on resource constrained devices. Finally I use <a href="https://platform.uno/">Uno Platform</a> to implement a cross-platform user-interface that allows real-time inference using these models.</p>
<h2 id="bitizen">Bitizen</h2>
<p>At <a href="https://www.bitizen.uk/">Bitizen</a> we are working to revitalize democracy by promoting understanding of - and engagement with - politics in the UK. As a first step towards this goal, we have built a platform which is able to ingest hundreds of forms of data from across the political landscape and present this data to users as meaningful information. Much of this data is unstructured text so we use state-of-the-art machine learning models to help us analyse, categorise and summarise the data in a manner which facilitates downstream processing (i.e. cataloging, searching, presentation, etc).</p>
<p>Given that we are a .NET shop and that most research around ML and AI takes place using either R or Python, we usually deploy models by containerizing them in their native environment accompanied by an HTTP API. This allows us to call the model from .NET and works beautifully in our containerized, event-driven architecture.</p>
<p>However, as we move towards promoting engagement, we wanted our smartphone app to be... well... smart. For example, while users were interacting with the app (i.e. contributing to a discussion, searching for additional information, etc) we wanted to be able to perform inferences similar to those we run on the backend on the device itself. Privacy and latency considerations meant calling a hosted endpoint wasn't really a great solution so we started looking round for alternatives.</p>
<p>This is what we came up with...</p>
<h3 id="a-quick-call-to-arms">A quick call-to-arms</h3>
<p>Bitizen is currently looking for a web-developer and/or designer to help improve our online presence and bring some of our app smarts to the web. If you have an interest in UK politics and like the idea of working with a intrepid, young, bootstrapped start-up, please do <a href="mailto:ian&#64;bitizen.uk">drop us a line</a> as we'd love to hear from you.</p>
<h2 id="ml.net-vs-nlp">ML.NET vs NLP</h2>
<p>Microsoft has a fairly strong ML offering for .NET developers in <a href="https://dotnet.microsoft.com/apps/machinelearning-ai/ml-dotnet">ML.NET</a>. Indeed, I illustrated ML.NET's capabilities in a blog post last year titled <a href="https://ian.bebbs.co.uk/posts/MLinUWP">'State-of-the-art ML in UWP'</a> which used a recent (at the time) ML model to perform salient object detection and image segmentation; a process very much suited to ML.NET strengths. Unfortunately the story around using ML.NET for NLP wasn't so strong and there were very few examples of how to use modern, <a href="https://en.wikipedia.org/wiki/Transformer_(machine_learning_model)">'transformer'</a> based models from within ML.NET.</p>
<p>Until, that is, <a href="https://github.com/GerjanVlot">Gjeran Vlot</a> published his <a href="https://github.com/GerjanVlot/BERT-ML.NET">BERT-ML.NET repository on GitHub</a>. In an incredibly concise and simple implementation, he illustrated how a <a href="https://en.wikipedia.org/wiki/BERT_(language_model)">BERT based</a> ONNX model could be used within ML.NET to perform 'Question Answering' (aka machine comprehension) based inference. This was fantastic and exactly what we had been looking for... except... we didn't want to perform (just) 'Question Answering' based inference. BERT - and related transformers - can be used for a broad variety of tasks including (but certainly not limited to) sentiment analysis, text classification and named entity recognition.</p>
<p>Given the other prepared models available in the <a href="https://github.com/onnx/models">ONNX Model Zoo</a> - from which Gjeran sourced his model - seemed fairly limited, we decided to go model hunting...</p>
<h2 id="hugging-face">Hugging Face</h2>
<p>If you've not been to <a href="https://huggingface.co/">Hugging Face</a> before, I would certainly recommend checking it out. Through the provision of excellent tooling and the formation of a vibrant, open community of users, Hugging Face have established themselves as the de-facto source for NLP models. On a single site you can explore, test and download models (with accompanying parameters and code) from a huge variety of sources (including Microsoft, Google and Elastic), pretrained (but with <a href="https://huggingface.co/transformers/training.html">fine-tuning recommended</a>) for a huge variety of use cases.</p>
<p>I decided that I wanted to initially try something that would give me quantifiable results (i.e. something more than just a probability) and knew that I wanted to try to run a model on an 'edge' (i.e. resource constrained) device. This meant finding an alternative to the BERT based models which are typically in excess of 400Mb.</p>
<p>Fortunately Hugging Face had me covered and, in short order, I had decided to use a <a href="https://huggingface.co/distilbert-base-uncased">DistilBERT</a> based model trained for Token Classification (aka Named Entity Recognition). After quickly experimenting with a few, I found <a href="https://huggingface.co/elastic/distilbert-base-cased-finetuned-conll03-english">a model by Elastic</a> that provided pretty good results and, at less than half the size of a comparable BERT model, seemed like it might be usable on an edge device.</p>
<h2 id="open-neural-network-exchange">Open Neural Network Exchange</h2>
<p>However, Hugging Face provides models for ease of consumption from it's own toolkit which usually means they're made available in either PyTorch or Tensorflow based formats. ML.NET, on the other hand, is only able to load models in the Open Neural Network Exchange (ONNX) format. This meant I needed to convert the models before I could use them.</p>
<p>Yet again, Hugging Face came to the rescue through the provision of <a href="https://huggingface.co/transformers/serialization.html">an API</a> which allows export of their models to ONNX. Knowing I would likely want to use multiple models in this manner (and not wanting to install various versions of Python on my workstation) I decided to build a docker container which would run the conversion and save the converted ONNX model to a mapped location. This proved to be shockingly easy with the image built using just a single Dockerfile containing:</p>
<pre><code>FROM python:latest
RUN pip install tensorflow
RUN pip install torch
RUN pip install transformers
RUN pip install keras2onnx
RUN pip install onnxruntime
ENTRYPOINT [ &quot;python&quot;, &quot;/usr/local/lib/python3.9/site-packages/transformers/convert_graph_to_onnx.py&quot; ]
</code></pre>
<p>This could then be run from Powershell like this:</p>
<pre><code>docker run --rm -v ${PWD}/Output:/Output ibebbs/huggingfacetoonnx:latest --framework pt --opset 12 --pipeline ner --model elastic/distilbert-base-cased-finetuned-conll03-english /Output/elastic/distilbert-base-cased-finetuned-conll03-english.onnx
</code></pre>
<p>Whereupon the script will download the specified model (in this case 'elastic/distilbert-base-cased-finetuned-conll03-english') using the specified framework ('pt' for PyTorch, 'tf' for Tensorflow), convert it to ONNX (using opset 12) including layers for the specific pipeline (in this case 'ner' for named entity recognition) and finally write the converted model to the 'Output/elastic' subdirectory of the current folder.</p>
<p>Should you wish to use this docker image, it is available - accompanied by full usage instructions - on <a href="https://hub.docker.com/r/ibebbs/huggingfacetoonnx">Docker Hub</a> including a link to the <a href="https://github.com/ibebbs/HuggingFaceToOnnx">source repository</a>.</p>
<h2 id="netron">Netron</h2>
<p>After downloading and converting the model, we need to examine it to determine the shape of the input and output layers. This is very easily done with <a href="https://netron.app/">Netron</a>.</p>
<p>Shown below is (a small section of) the DistilBERT model. By clicking on the 'input_ids' node a side-pane is shown which includes all the information we need.</p>
<p><a data-fancybox="Netron" href="/Content/Unoonnx/Netron - Full.png"><img src="/Content/Unoonnx/Netron - Full.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Neutron"/></a></p>
<p>As can be seen, Netron shows us the two inputs to the model: <code>input_ids</code> and <code>attention_mask</code>, both of which being two dimensional arrays of <code>Int64</code> values. It also shows us the output from the model: <code>output_0</code>, a three dimensional array of <code>float</code>.</p>
<h3 id="model-input">Model Input</h3>
<p>While using this model for inference, the <code>attention_mask</code> input is simply filled with 1s (each token has equal attention) so we will not discuss this input any further. Equally we will not be using multiple batches in this project so the <code>batch</code> dimension can be ignored leaving us with a single, <code>sequence</code> dimension of values to fill for <code>input_ids</code>.</p>
<p>In this model, the size of the <code>sequence</code> dimension is not specified illustrating that this model can accept dynamically sized input. As such, should we wanted to perform NLP on the sentence &quot;Sarah lives in London and works for Acme Corporation&quot;, we might expect to provide something like this to the <code>input_ids</code> input:</p>
<table class="table">
<thead>
<tr>
<th style="text-align: right;">Sequence</th>
<th>Word</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">0</td>
<td>Sarah</td>
</tr>
<tr>
<td style="text-align: right;">1</td>
<td>lives</td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td>in</td>
</tr>
<tr>
<td style="text-align: right;">3</td>
<td>London</td>
</tr>
<tr>
<td style="text-align: right;">4</td>
<td>and</td>
</tr>
<tr>
<td style="text-align: right;">5</td>
<td>works</td>
</tr>
<tr>
<td style="text-align: right;">6</td>
<td>for</td>
</tr>
<tr>
<td style="text-align: right;">7</td>
<td>Acme</td>
</tr>
<tr>
<td style="text-align: right;">8</td>
<td>Corporation.</td>
</tr>
</tbody>
</table>
<p>But, as can be seen above, the model accepts integers, not strings, so we must first 'tokenize' the input using a vocabulary specific to this model. This is done by downloading the vocabulary for the model from Hugging Face (available <a href="https://huggingface.co/elastic/distilbert-base-cased-finetuned-conll03-english/blob/main/vocab.txt">here</a>) then using a specific tokenizer to convert the input text into a series of tokens in a format the model expects; for BERT based models, a <a href="https://machinelearnit.com/2018/08/19/wordpiece-tokenisation/">&quot;WordPiece Tokenizer&quot;</a> is used.</p>
<p>Fortunately for us, we're able to use the &quot;WordPieceTokenizer&quot; provided in Gjeran's <a href="https://github.com/GerjanVlot/BERT-ML.NET/blob/master/Microsoft.ML.Models.BERT/Tokenizers/WordPieceTokenizer.cs">BERT-ML.NET repository</a>. Running the above input through this tokenizer would give use the following <code>input_ids</code> value:</p>
<table class="table">
<thead>
<tr>
<th style="text-align: right;">Sequence</th>
<th style="text-align: right;">Token Id</th>
<th>Token</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">0</td>
<td style="text-align: right;">101</td>
<td>[CLS]</td>
</tr>
<tr>
<td style="text-align: right;">1</td>
<td style="text-align: right;">21718</td>
<td>sa</td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td style="text-align: right;">10659</td>
<td>##rah</td>
</tr>
<tr>
<td style="text-align: right;">3</td>
<td style="text-align: right;">2491</td>
<td>lives</td>
</tr>
<tr>
<td style="text-align: right;">4</td>
<td style="text-align: right;">1107</td>
<td>in</td>
</tr>
<tr>
<td style="text-align: right;">5</td>
<td style="text-align: right;">25338</td>
<td>lo</td>
</tr>
<tr>
<td style="text-align: right;">6</td>
<td style="text-align: right;">17996</td>
<td>##ndon</td>
</tr>
<tr>
<td style="text-align: right;">7</td>
<td style="text-align: right;">1105</td>
<td>and</td>
</tr>
<tr>
<td style="text-align: right;">8</td>
<td style="text-align: right;">1759</td>
<td>works</td>
</tr>
<tr>
<td style="text-align: right;">9</td>
<td style="text-align: right;">1111</td>
<td>for</td>
</tr>
<tr>
<td style="text-align: right;">10</td>
<td style="text-align: right;">170</td>
<td>a</td>
</tr>
<tr>
<td style="text-align: right;">11</td>
<td style="text-align: right;">1665</td>
<td>##c</td>
</tr>
<tr>
<td style="text-align: right;">12</td>
<td style="text-align: right;">3263</td>
<td>##me</td>
</tr>
<tr>
<td style="text-align: right;">13</td>
<td style="text-align: right;">9715</td>
<td>corporation</td>
</tr>
<tr>
<td style="text-align: right;">14</td>
<td style="text-align: right;">119</td>
<td>.</td>
</tr>
<tr>
<td style="text-align: right;">15</td>
<td style="text-align: right;">102</td>
<td>[SEP]</td>
</tr>
</tbody>
</table>
<p>And it's these 'Token Ids' that are the input to our model.</p>
<h3 id="model-output">Model Output</h3>
<p>As we can see, the <code>output_0</code> layer consists of the same <code>batch</code> and <code>sequence</code> dimensions but adds an additional dimension with 9 elements. This additional dimension contains the probability of the token at <code>[batch,sequence]</code> belonging to a specific classification. The labels for each classifications are provided by the model's <a href="https://huggingface.co/elastic/distilbert-base-cased-finetuned-conll03-english/blob/main/config.json">'config.json' file on Hugging Face</a> as shown below:</p>
<pre><code class="language-json">{
...
&quot;id2label&quot;: {
&quot;0&quot;: &quot;O&quot;,
&quot;1&quot;: &quot;B-PER&quot;,
&quot;2&quot;: &quot;I-PER&quot;,
&quot;3&quot;: &quot;B-ORG&quot;,
&quot;4&quot;: &quot;I-ORG&quot;,
&quot;5&quot;: &quot;B-LOC&quot;,
&quot;6&quot;: &quot;I-LOC&quot;,
&quot;7&quot;: &quot;B-MISC&quot;,
&quot;8&quot;: &quot;I-MISC&quot;
},
...
}
</code></pre>
<p>As can be seen, this model uses <a href="https://en.wikipedia.org/wiki/Inside%E2%80%93outside%E2%80%93beginning_(tagging)"><code>Inside-outside-beginning</code> tagging</a> to delineate the beginning and inside of a specific classification from other classifications but, for the most part we can just treat this as 5 classifications:</p>
<ol start="0">
<li>Other</li>
<li>Person</li>
<li>Organisation</li>
<li>Location</li>
<li>Misc</li>
</ol>
<h2 id="bertonnx">BertONNX</h2>
<p>Armed with the model and an understanding of how to provide input/interpret output, I spiked out a quick .NET Core test project. Looking to simplify Gjeran's implementation even further I ended up with end-to-end, command line based inference engine in just 7 classes (including Gjeran's WordPieceTokenizer along with a Hugging Face configuration deserializer).</p>
<p>Should you wish to take a look, the source for this spike can be found in my <a href="https://github.com/ibebbs/BertOnnx">BertOnnx repository on Github</a>.</p>
<p>By far the biggest headache was working out how to shape the input (<code>Feature</code>) and output (<code>Result</code>) types to match the expected model shapes. ML.NET uses <code>[ColumnName([name])]</code> and <code>[VectorType([x,y])]</code> property attributes to bind properties to the model but, given the model was capable of processing dynamically sized input, I wasn't sure what values to use for the <code>VectorType</code> attribute.</p>
<p>Initially I tried omitting shape information from the attribute (<code>[VectorType]</code>) whereupon the app unceremoniously crashed with the error &quot;Variable length input columns not supported&quot;. A little searching revealed that this error meant exactly what it said and we couldn't use dynamically sized input with ML.NET!</p>
<p>So, instead I elected to use try a different approach and pad all input to a specific size (256 elements). This gave me <code>Feature</code> and <code>Result</code> types that looked like this:</p>
<pre><code>public class Feature
{
[VectorType(1, 256)]
[ColumnName(&quot;input_ids&quot;)]
public long[] Tokens { get; set; }
[VectorType(1, 256)]
[ColumnName(&quot;attention_mask&quot;)]
public long[] Attention { get; set; }
}
</code></pre>
<pre><code>public class Result
{
[VectorType(1,256,9)]
[ColumnName(&quot;output_0&quot;)]
public float[] Output { get; set; }
}
</code></pre>
<p>After this it was fairly plain sailing and in short order I had this:</p>
<p><a data-fancybox="BertONNX - Full" href="/Content/Unoonnx/BertONNX - Full.gif"><img src="/Content/Unoonnx/BertONNX - Full.gif" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="BertONNX"/></a></p>
<p>As you can see, the DistilBERT model correct identifies 'Sarah' as a 'B-PER' (person), 'London' as a 'B-LOC' (location) and 'Acme' as a 'B-ORG' (organisation) in just 202ms. Perfect!</p>
<h2 id="quantization">Quantization</h2>
<p>While having a custom built inference engine was pretty cool, I was a little concerned about memory consumption if I wanted to use the model on edge devices. Despite the DistilBERT model being significantly smaller than full BERT, memory consumption during inference hit around 1Gb. This would almost certainly be a stretch for many of the devices I'd like to run this model on.</p>
<p>Fortunately, ONNX has a little trick up it's sleeve called <a href="https://www.onnxruntime.ai/docs/how-to/quantization.html">'Quantization'</a>.</p>
<p>Quoting <a href="https://medium.com/microsoftazure/faster-and-smaller-quantized-nlp-with-hugging-face-and-onnx-runtime-ec5525473bb7">this article</a> on the matter:</p>
<blockquote class="blockquote">
<p>Quantization approximates floating-point numbers with lower bit width numbers, dramatically reducing memory footprint and accelerating performance. Quantization can introduce accuracy loss since fewer bits limit the precision and range of values. However, researchers have extensively demonstrated that weights and activations can be represented using 8-bit integers (INT8) without incurring significant loss in accuracy.</p>
<p>Compared to FP32, INT8 representation reduces data storage and bandwidth by 4x, which also reduces energy consumed. In terms of inference performance, integer computation is more efficient than floating-point math.</p>
</blockquote>
<p>Incredibly, quantizing a model using Hugging Face's ONNX export is as simple as specifying a <code>--quantize</code> flag. This meant generating a quantized version of the model took no more than effort than just running the following command:</p>
<pre><code>docker run --rm -v ${PWD}/Output:/Output ibebbs/huggingfacetoonnx:latest --framework pt --opset 12 --pipeline ner --model elastic/distilbert-base-cased-finetuned-conll03-english --quantize /Output/quantized-distilbert-base-cased-finetuned-conll03-english/model.onnx
</code></pre>
<p>The quantized version of the model was just 64Mb (75% smaller) and, due to it's input and output layers remaining unchanged, it was a drop in replacement for the unquantized model. Running with the quantized version resulted in:</p>
<p><a data-fancybox="BertONNX - Quantized" href="/Content/Unoonnx/BertONNX - Quantized.gif"><img src="/Content/Unoonnx/BertONNX - Quantized.gif" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="BertONNX"/></a></p>
<p>As you can see, the model loaded significantly faster and inference speed also got a boost. Best of all, memory consumption during inference was reduced to just 265Mb, definitely within the realms of possibility for an edge device.</p>
<p>Buoyed by this success, I pushed on to...</p>
<h2 id="unoonnx">UnoOnnx</h2>
<p>As per the initial driver for this exploration, I wanted an app on an edge device that would allow me to perform interactive inference. Knowing that <a href="https://platform.uno/">Uno Platform</a> could easily create apps that run across a variety of devices, I decided to whip up an app to do just this.</p>
<p>And so was born UnoOnnx ('Oo-noo-nx'?):</p>
<video class="img-responsive" style="margin: auto; width:66%; margin-top: 6px; margin-bottom: 6px;" controls>
<source src="/Content/Unoonnx/UnoOnnx - Windows.mp4" type="video/mp4"/>
Your browser does not support the video tag
</video>
<p>As you can see, the first inference is quite slow as it (lazily) loads the model but subsequent inferences are more than fast enough for an interactive app.</p>
<p>Then, with a little Uno Platform magic, I ran exactly the same code under Linux ('Oo-noo-nux'?):</p>
<video class="img-responsive" style="margin: auto; width:66%; margin-top: 6px; margin-bottom: 6px;" controls>
<source src="/Content/Unoonnx/UnoOnnx - Linux.mp4" type="video/mp4"/>
Your browser does not support the video tag
</video>
<p>(BTW, Loading the model isn't usually that slow - my machine was busy doing something else while I recorded this video).</p>
<p>Pretty Neat!</p>
<p>As with BertOnnx, the source for UnoOnnx is <a href="https://github.com/ibebbs/UnoOnnx">available on GitHub</a> if you want to take a look.</p>
<h2 id="moving-forward">Moving Forward</h2>
<p>In a subsequent post - and assuming there's sufficient interest - I hope to illustrate how to run these models on mobile devices (i.e. Android &amp; iOS). If this is of interest to you, please drop me a tweet and/or star the repositories above to let me know.</p>
<h2 id="conclusion">Conclusion</h2>
<p>As you can see, with the right toolset and a little bit of knowledge, it is fairly straight forward to use state-of-the-art machine learning models from .NET even within the resource constrained environment of an 'edge' device. While some use-cases that depend on sequence length (i.e. sentiment analysis) might be tricky to implement effectively in ML.NET, many other uses (text generation/classification, machine comprehension, translation, etc) should be pretty much pattern part.</p>
<p>However, working through the above has left me extremely concerned about Microsoft's strategy towards desktop (i.e. non-web) development. It seems to me that many of Microsoft's frameworks and SDKs for .NET desktop development are suffering from a distinct lack of resourcing/focus meaning development is slow and the frameworks are getting left behind by other languages/platforms. For example, here is the commit chart of ML.NET comparer to Hugging Face's native API:</p>
<table>
<tr>
<td>
<a data-fancybox="CommitComparison" href="/Content/Unoonnx/ML Commits.png"><img src="/Content/Unoonnx/ML Commits.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="ML.NET"/></a>
</td>
<td>
<a data-fancybox="CommitComparison" href="/Content/Unoonnx/Huggingface Commits.png"><img src="/Content/Unoonnx/Huggingface Commits.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="HuggingFace"/></a>
</td>
</tr>
</table>
<p>I think you'll agree, one of these projects looks significantly healthier than the other.</p>
<p>Furthermore, Microsoft's strategy/execution around UWP/WinUI/Project Reunion is an <strong>utter shambles</strong>. While I understand WinUI 3.0 is very new and Project Reunion still in preview, I honestly couldn't believe how poor the development experience was with these technologies.</p>
<p><em><strong>&#64;Microsoft, were it not for Uno Platform providing at least a modicum of continuity through the disastrous landscape that is Windows UI development, I - and I believe many others - would have jumped ship to other UI platforms a long time ago.</strong></em>
<em><strong>Please step up your game here. Many of us who have stuck with Windows UI technologies despite its fragmented and frustrating history really are getting to the end of our tether.</strong></em></p>
<h2 id="finally">Finally</h2>
<p>If you're interested in deploying state-of-the-art machine learning models within .NET or using the Uno Platform to deliver cross-platform apps, then please feel free to drop me a line using any of the links below or from my <a href="https://ian.bebbs.co.uk/about">about page</a>. As a freelance software developer and .NET consultant I'm always interested in hearing from potential new clients or ideas for new collaborations.</p>
</content:encoded>
</item>
<item>
<title>Using GMail To Send Email From A Custom Domain</title>
<link>http://ian.bebbs.co.uk/posts/UsingGMailForCustomDomainEmail</link>
<description><p>Do you use a domain registrar that provides email forwarding facilities? Then read this post to see how to set up Gmail to ensure the correct "From" address when replying to emails sent to your domains.</p></description>
<guid>http://ian.bebbs.co.uk/posts/UsingGMailForCustomDomainEmail</guid>
<pubDate>Mon, 04 Jan 2021 00:00:00 GMT</pubDate>
<content:encoded><h2 id="tldr">TL;DR</h2>
<p>Do you use a domain registrar that provides email forwarding facilities? Then read this post to see how to set up Gmail to ensure the correct &quot;From&quot; address when replying to emails sent to your domains.</p>
<h2 id="intro">Intro</h2>
<p>I am currently working with a couple of co-founders on an early stage start-up about which I hope to share more information shortly. Part of the prep work for this start-up has been registering a domain name and establishing communications channels.</p>
<p>To do this, I use a domain registrar that provides free email forwarding facilities. This is great as it allows you to receive email sent to '[person]&#64;[yourdomain.com]' as part of your regular email account. However, if you subsequently reply to email you received this way, your reply will be &quot;From&quot; your regular email account, not '[person]&#64;[yourdomain.com]'.</p>
<p>In this post I detail how to establish a completely free façade for email communication via your domain using a personal GMail account to receive and reply to email using a custom domain email addresses.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>Prior to following the steps below, you should ensure that you've enabled Two-Factor Authentication (2FA) on your GMail account. While it is possible to follow these steps without 2FA, this is not recommended as 2FA is basic practice for good security.</p>
<p>You should also ensure you're able to receive email forwarded by your domain registrar in your GMail account. As each domain registrar will provide different mechanisms for setting up and maintaining email forwarding rules, it will not be covered as part of this post. So, before following the steps below, you should ensure you're able to send an email to '[person]&#64;[yourdomain.com]' and receive it in your GMail account.</p>
<h2 id="app-password">App Password</h2>
<p>Before being able to set up &quot;Send mail as&quot; functionality, you'll need an 'App Password'. An 'App Password' allows software/authentication flows that aren't compatible with 2FA to successfully authenticate with Google Services and is essentially just a 'special' password that is a) highly complex - to prevent brute-force attacks, and b) can be revoked should it ever be compromised.</p>
<p>To set up an 'App Password', from your GMail inbox click your account icon in the top right hand corner, then click &quot;Manage your Google Account&quot; as shown below:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/ManageGMailAccount.png"><img src="/Content/UsingGMailForCustomDomainEmail/ManageGMailAccount.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Manage GMail Account"/></a></p>
<p>This will open a new tab for managing your Google Account. Select the &quot;Security&quot; category from the menu on the left (or across the top on smaller devices) and scroll down to the &quot;Signing in to Google&quot; section. Here, you should see that 2-Step verification has been turned on (if it isn't, turn it on now before continuing) and an &quot;App passwords&quot; option as shown below:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/SigningInToGoogle.png"><img src="/Content/UsingGMailForCustomDomainEmail/SigningInToGoogle.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Signing In To Google"/></a></p>
<p>Click the &quot;App passwords&quot; option to create a new App password. In the &quot;Select app&quot; drop-down select &quot;Other (Custom name)&quot; and enter a name for the app password (I tend to use the domain name). Once this is entered click the Generate button as shown below:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/AddAppPassword.png"><img src="/Content/UsingGMailForCustomDomainEmail/AddAppPassword.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Add App Password"/></a></p>
<p>Clicking &quot;Generate&quot; will display a dialog containing your new app password as shown below. Copy this password and keep it safe (I would recommend adding it to your password manager).</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/NewAppPassword.png"><img src="/Content/UsingGMailForCustomDomainEmail/NewAppPassword.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="New App Password"/></a></p>
<p>You can now close the &quot;Google Account&quot; tab and return to Gmail.</p>
<h2 id="send-mail-as">Send Mail As</h2>
<p>Back in GMail, click the 'cog' icon in the top right to display &quot;Quick Settings&quot; and, from there, click the &quot;See all settings&quot; button as shown below:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/SeeAllGmailSettings.png"><img src="/Content/UsingGMailForCustomDomainEmail/SeeAllGmailSettings.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="See All Gmail Settings"/></a></p>
<p>This will take you to the Settings page where you should see an &quot;Accounts and Import&quot; category. Click this category to reveal the &quot;Send mail as&quot; options as shown below:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/SendMailAs.png"><img src="/Content/UsingGMailForCustomDomainEmail/SendMailAs.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Send Mail As"/></a></p>
<p>Click the &quot;Add another email address&quot; link which will display a new window allowing you to &quot;Enter information about your other email address&quot;. In the &quot;Email Address&quot; text box, enter the email address of the domain account set up to forward email to your Gmail account as shown below:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/EnterEMailAddress.png"><img src="/Content/UsingGMailForCustomDomainEmail/EnterEMailAddress.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Enter EMail Address"/></a></p>
<p>Click &quot;Next Step &gt;&gt;&quot; to reveal the &quot;Send emails through your SMTP server&quot; page. This is where we get clever and, instead of entering the details of our own SMTP server (which used to work until Google changed requirements a while back) we're instead going to send through GMails own servers. In the 'SMTP Server' text box enter 'smtp.gmail.com' then in the 'Username' text box enter your GMail username (i.e. me&#64;gmail.com). Finally, in the 'Password' box, enter the App Password we generated in the previous section. Once everything is entered the dialog should look similar to this:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/SMTPServerConfiguration.png"><img src="/Content/UsingGMailForCustomDomainEmail/SMTPServerConfiguration.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="SMTP Server Configuration"/></a></p>
<p>Click &quot;Add Account &gt;&gt;&quot; button which, if everything was entered correctly, should take you to the &quot;Confirm verification and add your email address&quot; screen. Here you are prompted for a confirmation code as shown below:</p>
<p><a data-fancybox="gmail" href="/Content/UsingGMailForCustomDomainEmail/VerifyEmailAddress.png"><img src="/Content/UsingGMailForCustomDomainEmail/VerifyEmailAddress.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Verify Email Address"/></a></p>
<p>Leaving the dialog window open, return to your GMail inbox where you should have received an email from &quot;Gmail Team&quot; titled &quot;Gmail Confirmation - Send Mail as [person]&#64;[yourdomain.com]&quot;. In this email you should see a confirmation code which you can copy and paste into the &quot;Confirm verification and add your email address&quot; dialog window.</p>
<p>If you don't receive this email then you should double check your email forwarding rules to ensure the account your entered in the dialog above is configured to forward to your Gmail account. Also bear in mind that some domain registrars can take quite a while to forward email for new accounts (I've seen up to 24 hours previously) so, if you've confirmed that your settings are correct, you may simply need to be a little patient here.</p>
<p>Anyway, assuming your received the confirmation code email and pasted it into the &quot;Enter and verify the confirmation code&quot; text box, you should be able to click Verify, at which point the dialog will disappear. Congratulations, you're now able to send email using your domain email address!</p>
<h2 id="sending-email">Sending Email</h2>
<p>Now, when composing a new email, you're able to click the &quot;From&quot; name and select the email address you want the recipient to see. Furthermore, when you receive email sent to the domain email address, GMail will automatically use this address as the &quot;From&quot; account when replying.</p>
<p>Enjoy!</p>
</content:encoded>
</item>
<item>
<title>Cross-Platform App Authentication with Azure AD B2C And The Uno Platform</title>
<link>http://ian.bebbs.co.uk/posts/UnoB2C</link>
<description><p>In this post I comprehensively show how apps written using the <a href="https://platform.uno/">Uno Platform</a> can leverage <a href="https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/">Azure AD B2C</a> &amp; <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet">MSAL.Net</a> to provide Identity and Access Management services across platforms as diverse as Windows, Android, iOS and the web. As you will see, this combination of technologies provides extremely cheap, simple and flexible identity management functionality that runs from a single code base.</p></description>
<guid>http://ian.bebbs.co.uk/posts/UnoB2C</guid>
<pubDate>Wed, 11 Nov 2020 00:00:00 GMT</pubDate>
<content:encoded><h2 id="tldr">TL;DR</h2>
<p>In this post I comprehensively show how apps written using the <a href="https://platform.uno/">Uno Platform</a> can leverage <a href="https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/">Azure AD B2C</a> &amp; <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet">MSAL.Net</a> to provide Identity and Access Management services across platforms as diverse as Windows, Android, iOS and the web. As you will see, this combination of technologies provides extremely cheap, simple and flexible identity management functionality that runs from a single code base.</p>
<h2 id="intro">Intro</h2>
<p>Seamless identity management in client-facing apps is critically important to customer engagement yet extremely difficult to implement correctly. In recent years, numerous IDentity as a Service (IDaaS) providers have emerged to help developers address this challenge yet somehow secure authentication and authorization remain one of the most arduous parts of app development.</p>
<p>In this article I present a suite of technologies that can be leveraged to provide identity management in a simple and affordable yet flexible and scalable manner. I show how recent changes to these technologies allow you to leverage the most recent and secure authentication flows (i.e. <a href="https://romikoderbynew.com/2019/09/20/oauth-2-0-authorization-code-with-pkce-vs-implicit-grant/#:%7E:text=Does%20your%20Authorization%20Server%20supprot%20CORS%3F%20Can,your%20clients%20use%20modern%20browsers%20that%20support%20CORS%3F">&quot;Authorization Code with PKCE&quot; instead of &quot;Implicit Grant&quot;</a>) and I illustrate how this technology stack can be used to implement apps that run across all major platforms - including the web - without the need for the developer to maintain onerous platform-specific code.</p>
<p>Finally, much of this post is composed of information from - and links to - other articles from around the web. I have aggregated and annotated these posts below such that the reader is provided a comprehensive guide to using these technologies within a cross-platform Uno application. While I specifically discuss only the major platforms (UWP, Android, iOS and Web) the approaches used below should be pertinent to any platform supported by Uno.</p>
<h2 id="technologies">Technologies</h2>
<p>The suite of technologies used in this article is comprised of: <a href="https://platform.uno/">Uno Platform</a>, <a href="https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/">Azure AD B2C</a> and <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet">MSAL.Net</a>. I provide a brief introduction to these technologies below before proceeding to show how they can be combined to provide a holistic cloud-based user-management solution.</p>
<h3 id="uno-platform">Uno Platform</h3>
<p>Regular readers of my blog will be well aware of the Uno Platform by now but, for new readers, the Uno Platform allows UWP apps to run <em>natively</em> on every major platform including desktop (Windows, Mac, Linux), mobile (Android &amp; iOS) and the web (in pretty much any browser). It achieves this by implementing WinRT APIs on top of Xamarin (for desktop/mobile) and WASM (for the web) which allows the developer to write a single code-base which can be transparently shared across each of these platforms.</p>
<p>I have <a href="https://ian.bebbs.co.uk/tags/uno-platform">blogged about Uno Platform extensively over the past year</a> as, in my opinion, it represents the best platform for cross-platform UI develop and empowers developers to utilise a <a href="https://ian.bebbs.co.uk/posts/UnoValue">&quot;one-stack&quot; solution architecture</a>. My consultancy - <a href="https://www.cogenity.com/">Cogenity</a> - specialise in providing support for, and bespoke development of, cross-platform applications written using the Uno Platform. Should you have any questions regarding this article or the Uno Platform in general, please feel free to <a href="https://www.cogenity.com/#three">drop us a line</a> - we love to hear about applications being built with Uno and help our clients deliver on the promise of this amazing technology.</p>
<h3 id="azure-ad-b2c">Azure AD B2C</h3>
<p>Azure Active Directory B2C (AAD B2C) is Microsoft's Azure based Identity and Access Management (IAM) offering for business-to-consumer (B2C) applications. Unlike regular <a href="https://azure.microsoft.com/en-us/services/active-directory/">Azure Active Directory</a> which is very much aimed at B2B and LoB applications, AAD B2C has been designed from the ground up for providing seamless IAM for customer-facing apps. As such it allows the developer to easily leverage advanced scenarios such as social login and multi-factor authentication while simultaneous providing the means to customise &quot;every pixel of the registration and sign-in experience&quot;.</p>
<p>Amazingly, this service is offered at an incredibly low price-point. The first 50,000 monthly actives users are free and subsequent users cost just £0.002423p/m! This is easily enough to bootstrap an application and gain market traction prior to being faced with a significant bill for IDaaS and, in any event, these costs will almost certainly be less than the cost of writing and hosting a bespoke solution.</p>
<h3 id="msal.net">MSAL.Net</h3>
<p>Microsoft Authentication Library for .NET (MSAL.NET) is Microsoft's successor to Active Directory Authentication Library for .NET (ADAL.NET). It is part of the <a href="https://docs.microsoft.com/en-gb/azure/active-directory/develop/v2-overview">Microsoft Identify Platform for Developers</a> and represents current best practise for Azure AD authentication from .NET applications.</p>
<p>As we will see below, authentication with MSAL.NET is really very simple and works beautifully in cross-platform scenarios on the Uno Platform.</p>
<h2 id="getting-started-with-azure-ad-b2c">Getting started with Azure AD B2C</h2>
<p>So, with introductions out the way, let get started with Azure AD B2C by creating a new tenant. This is by far the most complicated part of the process and covering it in detail could easily balloon this post to an unmanageable size. Fortunately <a href="https://twitter.com/CodeMillMatt">Matthew Soucoup</a> has covered all the steps for creating a Azure AD B2C tenant on his <a href="https://codemilltech.com/">blog</a>. In the steps below I will be pointing you to Matt's blog posts which I very much encourage you to read and follow before continuing.</p>
<h3 id="step-1-understanding-terminology">Step 1 - Understanding terminology</h3>
<p>One of the most confusing parts of authentication is understanding the various terminology. In his <a href="https://codemilltech.com/xamarin-authentication-with-azure-active-directory-b2c/">first post about Azure AD B2C</a> Matt digs into the various terminology you'll need to understand in order to correctly setup and use Azure AD B2C. If you're at all unsure about terms such as Tenant, Providers or Policies, I'd very much recommend a read of this post before continuing.</p>
<h3 id="step-2-creating-a-tenant">Step 2 - Creating A Tenant</h3>
<p>Now we understand the terminology used, we can go ahead and create an Azure AD B2C Tenant. Again, Matt covers this fantastically well in a <a href="https://codemilltech.com/creating-an-ad-b2c-tenant/">blog post</a>. He also covers the process in a <a href="https://www.youtube.com/watch?v=zfyHwD9sJJ4&amp;feature=youtu.be">YouTube</a> video which helps convey some of the &quot;tricky&quot; behaviour of Azure Directories. Read or view either of these links and follow the steps therein. Once complete you should have a new &quot;[tenant].onmicrosoft.com&quot; directory with an Azure AD B2C service as shown here:</p>
<img src="/Content/UnoB2C/New B2C Tenant.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="New B2C Tenant"/>
<h3 id="step-3-adding-a-policy">Step 3 - Adding A Policy</h3>
<p>While the Azure AD B2C Tenant provides the infrastructure for cloud-based IDaaS, policies dictate who can use this service and how. In order for users of your app to be able to register and/or log in, you need to create a &quot;User flow&quot; policy in your tenant. Matt covers this process in the &quot;Creating A Policy&quot; section of this <a href="https://codemilltech.com/adding-authentication-and-authorization-with-azure-ad-b2c/#creatingapolicy">blog post</a> however the post is slightly out of date as the Azure Portal has changed significantly since he authored it. I would suggest reading Matt's blog post so you understand the process then following the screen shots shown below (tap to enlarge):</p>
<table>
<tr>
<td><a data-fancybox="addingapolicy" href="/Content/UnoB2C/New User Flow.png"><img src="/Content/UnoB2C/New User Flow.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="New User Flow"/></a></td>
<td><a data-fancybox="addingapolicy" href="/Content/UnoB2C/Sign Up and sign in flow.png"><img src="/Content/UnoB2C/Sign Up and sign in flow.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Sign Up and sign in flow"/></a></td>
</tr>
<tr>
<td style="text-align: center"><h5>1. Create a new user flow</h5></td>
<td style="text-align: center"><h5>2. Select recommended sign up and sign in flow</h5></td>
</tr>
<tr>
<td><a data-fancybox="addingapolicy" href="/Content/UnoB2C/New User Flow Name.png"><img src="/Content/UnoB2C/New User Flow Name.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="New User Flow Name"/></a></td>
<td><a id="newuserflowclaims" data-fancybox="addingapolicy" href="/Content/UnoB2C/New User Flow Claims.png"><img src="/Content/UnoB2C/New User Flow Claims.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="New User Flow Claims"/></a></td>
</tr>
<tr>
<td style="text-align: center"><h5>3. Name the user flow</h5></td>
<td style="text-align: center"><h5>4. Select registration attributes and token claims</h5></td>
</tr>
</table>
<p>Make sure you take note of your sign-up and sign-in flow name as you'll need this later.</p>
<h3 id="step-4-add-app-registration">Step 4 - Add App Registration</h3>
<p>The last step is to add an app registration. This controls how your app is expected to interact with Azure AD B2C and it's credentials for doing so. Again <a href="https://codemilltech.com/adding-authentication-and-authorization-with-azure-ad-b2c/#step2settinguptheazureadb2capplication">Matt has us covered</a> but again, his descriptions and screenshots are a little out of date. Furthermore we need to add a couple of &quot;platforms&quot; to the app registration in order to support the variety of operating systems and devices available to Uno applications.</p>
<p>The screen shots below show how to set up an app registration that leverages Authorization Code Flow with PKCE for UWP/WASM authentication and protocol activation for Android / iOS:</p>
<table>
<tr>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/App Registrations.png"><img src="/Content/UnoB2C/App Registrations.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="App Registrations"/></a></td>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/New App Registration.png"><img src="/Content/UnoB2C/New App Registration.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Add App Registration"/></a></td>
</tr>
<tr>
<td style="text-align: center"><h5>1. Navigate to app registration</h5></td>
<td style="text-align: center"><h5>2. Add a new registration</h5></td>
</tr>
<tr>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/Register an Application.png"><img src="/Content/UnoB2C/Register an Application.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Register the Application"/></a></td>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/Note Application Id.png"><img src="/Content/UnoB2C/Note Application Id.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Note Application Id"/></a></td>
</tr>
<tr>
<td style="text-align: center"><h5>3. Name the application and change Redirect URI</h5></td>
<td style="text-align: center"><h5>4. Note the application id and click Redirect URIs</h5></td>
</tr>
<tr>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/Confirm Authorization Code Flow with PKCE.png"><img src="/Content/UnoB2C/Confirm Authorization Code Flow with PKCE.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Confirm Authorization Code Flow with PKCE"/></a></td>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/Add a platform.png"><img src="/Content/UnoB2C/Add a platform.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Add a platform"/></a></td>
</tr>
<tr>
<td style="text-align: center"><h5>5. Confirm Authorization Code Flow with PKCE</h5></td>
<td style="text-align: center"><h5>6. Click 'Add a platform'</h5></td>
</tr>
<tr>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/Select Mobile and desktop applications.png"><img src="/Content/UnoB2C/Select Mobile and desktop applications.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Select Mobile and desktop applications"/></a></td>
<td><a data-fancybox="addappregistration" href="/Content/UnoB2C/Add msal redirect uri.png"><img src="/Content/UnoB2C/Add msal redirect uri.png" class="img-responsive" style="margin: auto; max-width:80%; margin-top: 6px; margin-bottom: 6px;" alt="Add msal redirect uri"/></a></td>
</tr>
<tr>
<td style="text-align: center"><h5>7. Select 'Mobile and desktop applications'</h5></td>
<td style="text-align: center"><h5>8. Add MSAL Redirect URI then click Configure</h5></td>
</tr>
</table>
<p>And there we go. We now have an Azure AD B2C tenant set up that is able to authenticate users using best practices across a variety of platforms. If everything is set up correctly, your tenant should look similar to this:</p>
<p><a data-fancybox="addappregistration" href="/Content/UnoB2C/SPA and desktop Redirect URIs.png"><img src="/Content/UnoB2C/SPA and desktop Redirect URIs.png" class="img-responsive" style="margin: auto; max-width:50%; margin-top: 6px; margin-bottom: 6px;" alt="SPA and desktop Redirect URIs"/></a></p>
<h2 id="create-an-uno-application">Create an Uno Application</h2>
<p>We'll now use Visual Studio to create a cross-platform Uno Platform application which is able to authenticate users using the Azure AD B2C tenant we set up above. If you're not sure how to create a new Uno Platform application then follow the steps <a href="https://platform.uno/docs/articles/getting-started-tutorial-1.html">here</a>. I'm going to name my project <code>UnoAuth</code>.</p>
<h3 id="install-dependencies">Install Dependencies</h3>
<p>We're going to need to install the following packages to all projects in the solution:</p>
<ol>
<li><a href="https://www.nuget.org/packages/Microsoft.Identity.Client">Microsoft.Identity.Client</a></li>
<li><a href="https://www.nuget.org/packages/Uno.UI.MSAL">Uno.UI.MSAL</a></li>
<li><a href="https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt">System.IdentityModel.Tokens.Jwt</a></li>
</ol>
<p>The easiest way to do this is the &quot;Manage Packages for Solution&quot; (via right-clicking on the solution in Solution Explorer) as shown here:</p>
<img src="/Content/UnoB2C/Manage Packages for Solution.png" class="img-responsive" style="margin: auto; max-width:66%; margin-top: 6px; margin-bottom: 6px;" alt="Manage Packages for Solution"/>
<h3 id="authentication-configuration">Authentication Configuration</h3>
<p>With the prerequisite dependencies installed we're going to provide the authentication settings required by Azure AD B2C. As some of these settings should be considered sensitive (i.e. the ClientId) we're going to use a partial class (<code>Authentication</code>) split between two files (<code>Authentication.cs</code> and <code>Authentication.Secrets.cs</code>) so that we can put access logic in one and sensitive values in the other. We can then ensure the second file doesn't get committed to source control (via <code>.gitignore</code>).</p>
<p>The <code>Authentication.cs</code> should look like this:</p>
<pre><code class="language-c#">using System.Collections.Generic;
namespace UnoAuth
{
public static partial class Authentication
{
// ClientIdSecret should be provided in `Authentication.Secrets.cs` as part of the
// partial class
public static string Tenant =&gt; TenantSecret;
// ClientIdSecret should be provided in `Authentication.Secrets.cs` as part of the
// partial class
public static string ClientId =&gt; ClientIdSecret;
// PolicySecret should be provided in `Authentication.Secrets.cs` as part of the
// partial class
public static string Policy =&gt; PolicySecret;
// RedirectUriSecret should be provided in `Authentication.Secrets.cs` as part of the
// partial class
#if __ANDROID__ || __IOS__
public static string RedirectUri =&gt; RedirectUriSecretDesktop;
#else
public static string RedirectUri =&gt; RedirectUriSecret;
#endif
#if __IOS__
// BundleNameSecret should be provided in `Authentication.Secrets.cs` as part of the
// partial class
public static string BundleName =&gt; BundleNameSecret;
#endif
// ScopesSecret should be provided in `Authentication.Secrets.cs` as part of the
// partial class
public static IEnumerable&lt;string&gt; Scopes =&gt; ScopesSecret;
public static string AuthorityBase =&gt; $&quot;https://{Tenant}.b2clogin.com/tfp/{Tenant}.onmicrosoft.com/&quot;;
public static string Authority =&gt; $&quot;{AuthorityBase}{Policy}&quot;;
public static string GivenNameClaimType =&gt; &quot;given_name&quot;;
}
}
</code></pre>
<p>Note the <code>#if ... #else ... #endif</code> compiler directives. These directives allows us to use <a href="https://platform.uno/docs/articles/platform-specific-csharp.html">platform specific code</a> such that the correct redirect URI is used on each platform and platform specific values can be provided only only the platforms that require them.</p>
<p>Next, <code>Authentication.Secrets.cs</code> should look like this (but with the appropriate values):</p>
<pre><code class="language-c#">using System.Collections.Generic;
namespace UnoAuth
{
public static partial class Authentication
{
// In this sample, this value will be &quot;bebbsauthspike&quot;
private static readonly string TenantSecret = &quot;[REPLACE THIS VALUE]&quot;;
// This is the ClientId value from the app registration.
// It will be in the form of &quot;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&quot;
private static readonly string ClientIdSecret = &quot;[REPLACE THIS VALUE]&quot;;
// In this sample, this value will be &quot;B2C_1_signup-signin&quot;
private static readonly string PolicySecret = &quot;[REPLACE THIS VALUE]&quot;;
// In this sample, this value will be &quot;http://localhost:5000&quot;
private static readonly string RedirectUriSecret = &quot;[REPLACE THIS VALUE]&quot;;
private static readonly string RedirectUriSecretDesktop = $&quot;msal{ClientIdSecret}://auth&quot;;
// In this sample, this value will be &quot;com.companyname.UnoAuth&quot;
private static readonly string BundleNameSecret = &quot;[REPLACE THIS VALUE]&quot;;
// Note, we're currently only interested in authenticating, not defining any additional scopes which a
// user may or may not have access to. As such, we only request access to the `openid` scope.
private static readonly IEnumerable&lt;string&gt; ScopesSecret = new[] { &quot;https://graph.microsoft.com/openid&quot; };
}
}
</code></pre>
<h3 id="create-the-ui">Create the UI</h3>
<p>Finally we're going to create the UI. Given our app will have three distinct states - Unauthenticated, Authenticating &amp; Authenticated - we're going to use <a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.VisualState?view=winrt-19041">visual states</a> to directly reflect these states in the UI. So, in <code>Main.xaml</code>, update the Xaml to the following:</p>
<pre><code class="language-xaml">&lt;Page
x:Class=&quot;UnoAuth.MainPage&quot;
xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
xmlns:d=&quot;http://schemas.microsoft.com/expression/blend/2008&quot;
xmlns:mc=&quot;http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
mc:Ignorable=&quot;d&quot;&gt;
&lt;Grid x:Name=&quot;StateGrid&quot; Background=&quot;{ThemeResource ApplicationPageBackgroundThemeBrush}&quot;&gt;
&lt;VisualStateManager.VisualStateGroups&gt;
&lt;VisualStateGroup x:Name=&quot;AuthenticationStates&quot;&gt;
&lt;VisualState x:Name=&quot;Unauthenticated&quot;/&gt;
&lt;VisualState x:Name=&quot;Authenticating&quot;&gt;
&lt;VisualState.Setters&gt;
&lt;Setter Target=&quot;AuthenticatingGrid.(UIElement.Visibility)&quot; Value=&quot;Visible&quot;/&gt;
&lt;Setter Target=&quot;AuthenticatedGrid.(UIElement.Visibility)&quot; Value=&quot;Collapsed&quot;/&gt;
&lt;Setter Target=&quot;UnauthenticatedGrid.(UIElement.Visibility)&quot; Value=&quot;Collapsed&quot;/&gt;
&lt;/VisualState.Setters&gt;
&lt;/VisualState&gt;
&lt;VisualState x:Name=&quot;Authenticated&quot;&gt;
&lt;VisualState.Setters&gt;
&lt;Setter Target=&quot;AuthenticatedGrid.(UIElement.Visibility)&quot; Value=&quot;Visible&quot;/&gt;
&lt;Setter Target=&quot;AuthenticatingGrid.(UIElement.Visibility)&quot; Value=&quot;Collapsed&quot;/&gt;
&lt;Setter Target=&quot;UnauthenticatedGrid.(UIElement.Visibility)&quot; Value=&quot;Collapsed&quot;/&gt;
&lt;/VisualState.Setters&gt;
&lt;/VisualState&gt;
&lt;/VisualStateGroup&gt;
&lt;/VisualStateManager.VisualStateGroups&gt;
&lt;Grid x:Name=&quot;UnauthenticatedGrid&quot; Visibility=&quot;Visible&quot; Background=&quot;#FF1D437C&quot;&gt;
&lt;StackPanel HorizontalAlignment=&quot;Center&quot; VerticalAlignment=&quot;Center&quot;&gt;
&lt;TextBlock Text=&quot;Click 'Sign In' To Authenticate&quot; TextWrapping=&quot;Wrap&quot; HorizontalAlignment=&quot;Center&quot; Style=&quot;{ThemeResource TitleTextBlockStyle}&quot; Margin=&quot;32,32,32,32&quot; Foreground=&quot;White&quot;/&gt;
&lt;Button x:Name=&quot;SignInButton&quot; HorizontalAlignment=&quot;Center&quot; Padding=&quot;32,16,32,16&quot; Margin=&quot;32,32,32,32&quot; Click=&quot;SignInButton_Click&quot; Background=&quot;#FF412663&quot;&gt;
&lt;TextBlock Text=&quot;Sign In&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{ThemeResource SubtitleTextBlockStyle}&quot; Foreground=&quot;White&quot;/&gt;
&lt;/Button&gt;
&lt;/StackPanel&gt;
&lt;/Grid&gt;
&lt;Grid x:Name=&quot;AuthenticatingGrid&quot; Visibility=&quot;Collapsed&quot; Background=&quot;#FFC07000&quot;&gt;
&lt;StackPanel Orientation=&quot;Vertical&quot; HorizontalAlignment=&quot;Center&quot; VerticalAlignment=&quot;Center&quot;&gt;
&lt;TextBlock HorizontalAlignment=&quot;Center&quot; Text=&quot;Authenticating&quot; Style=&quot;{ThemeResource TitleTextBlockStyle}&quot; Margin=&quot;32&quot; Foreground=&quot;White&quot;/&gt;
&lt;TextBlock HorizontalAlignment=&quot;Center&quot; Text=&quot;One Sec...&quot; Style=&quot;{ThemeResource SubtitleTextBlockStyle}&quot; Margin=&quot;32&quot; Foreground=&quot;White&quot;/&gt;
&lt;/StackPanel&gt;
&lt;/Grid&gt;
&lt;Grid x:Name=&quot;AuthenticatedGrid&quot; Visibility=&quot;Collapsed&quot; Background=&quot;#FF1F6900&quot;&gt;
&lt;StackPanel Orientation=&quot;Vertical&quot; HorizontalAlignment=&quot;Center&quot; VerticalAlignment=&quot;Center&quot;&gt;
&lt;TextBlock HorizontalAlignment=&quot;Center&quot; Style=&quot;{ThemeResource TitleTextBlockStyle}&quot; Margin=&quot;32&quot; Foreground=&quot;White&quot;&gt;
&lt;Run Text=&quot;Hi &quot;/&gt;&lt;Run Text=&quot;{x:Bind Path=GivenName, Mode=OneWay}&quot;/&gt;&lt;Run Text=&quot;!&quot;/&gt;
&lt;/TextBlock&gt;
&lt;TextBlock HorizontalAlignment=&quot;Center&quot; Text=&quot;How are you?&quot; Style=&quot;{ThemeResource SubtitleTextBlockStyle}&quot; Margin=&quot;32&quot; Foreground=&quot;White&quot;/&gt;
&lt;Button x:Name=&quot;SignOutButton&quot; HorizontalAlignment=&quot;Center&quot; Padding=&quot;32,16,32,16&quot; Margin=&quot;32,32,32,32&quot; Click=&quot;SignOutButton_Click&quot; Background=&quot;#FF412663&quot;&gt;
&lt;TextBlock Text=&quot;Sign Out&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{ThemeResource SubtitleTextBlockStyle}&quot; Foreground=&quot;White&quot;/&gt;
&lt;/Button&gt;
&lt;/StackPanel&gt;
&lt;/Grid&gt;
&lt;/Grid&gt;
&lt;/Page&gt;
</code></pre>
<p>Here you can see the three visual states named: <code>Unauthenticated</code>, <code>Authenticating</code> &amp; <code>Authenticated</code>. In the <code>Unauthenticated</code> state the <code>UnauthenticatedGrid</code> will be visible while both the <code>AuthenticatingGrid</code> and <code>AuthenticatedGrid</code> will be collapsed. This pattern is repeated in the other states (<code>Authenticating</code> only showing <code>AuthenticatingGrid</code> &amp; <code>Authenticated</code> only showing <code>AuthenticatedGrid</code>) such that only elements pertinent to the current state are displayed.</p>
<p>In the <code>UnauthenticatedGrid</code> we have a <code>SignInButton</code> from which we use the Click event handler to invoke the authentication process. While authentication is taking place, the <code>AuthenticatingGrid</code> will be shown which asks the user to wait. Finally in the <code>AuthenticatedGrid</code> we have a <code>TextBlock</code> which will shown the given name of the authenticated user and a <code>SignOutButton</code> which allows the user to sign-out.</p>
<h3 id="implement-the-code">Implement the Code</h3>
<p>In the <code>MainPage.xaml.cs</code> code-behind file we implement the <code>SignInButton_Click</code> method to perform authentication using Azure AD B2C and the <code>SignOutButton_Click</code> method to remove the cached authentication tokens. Here's the code:</p>
<pre><code class="language-c#">using Microsoft.Identity.Client;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using Uno.UI.MSAL;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace UnoAuth
{
[TemplateVisualState(GroupName = AuthenticationStatesGroupName, Name = UnauthenticatedStateName)]
[TemplateVisualState(GroupName = AuthenticationStatesGroupName, Name = AuthenticatingStateName)]
[TemplateVisualState(GroupName = AuthenticationStatesGroupName, Name = AuthenticatedStateName)]
public sealed partial class MainPage : Page
{
private const string AuthenticationStatesGroupName = &quot;AuthenticationStates&quot;;
private const string UnauthenticatedStateName = &quot;Unauthenticated&quot;;
private const string AuthenticatingStateName = &quot;Authenticating&quot;;
private const string AuthenticatedStateName = &quot;Authenticated&quot;;
public static readonly DependencyProperty GivenNameProperty = DependencyProperty.Register(&quot;GivenName&quot;, typeof(string), typeof(MainPage), new PropertyMetadata(string.Empty));
private readonly IPublicClientApplication _authenticationClient;
public MainPage()
{
this.InitializeComponent();
_authenticationClient = PublicClientApplicationBuilder
.Create(Authentication.ClientId)
#if __IOS__
.WithIosKeychainSecurityGroup(Authentication.BundleName)
#endif
.WithB2CAuthority(Authentication.Authority)
.WithRedirectUri(Authentication.RedirectUri)
.WithUnoHelpers()
.Build();
}
private void TransitionToAuthenticated(AuthenticationResult authResult)
{
var token = new JwtSecurityToken(authResult.IdToken);
GivenName = token.Claims
.Where(claim =&gt; Authentication.GivenNameClaimType.Equals(claim.Type))
.Select(claim =&gt; claim.Value)
.First();
VisualStateManager.GoToState(this, AuthenticatedStateName, true);
}
private async void SignInButton_Click(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, AuthenticatingStateName, true);
try
{
var accounts = await _authenticationClient.GetAccountsAsync();
var result = await _authenticationClient
.AcquireTokenSilent(Authentication.Scopes, accounts.FirstOrDefault())
.ExecuteAsync();
TransitionToAuthenticated(result);
}
catch (MsalUiRequiredException)
{
try
{
var result = await _authenticationClient
.AcquireTokenInteractive(Authentication.Scopes)
.WithPrompt(Prompt.ForceLogin)
.WithUnoHelpers()
.ExecuteAsync();
TransitionToAuthenticated(result);
}
catch
{
// Something went wrong, the the user try again
VisualStateManager.GoToState(this, UnauthenticatedStateName, true);
}
}
}
private async void SignOutButton_Click(object sender, RoutedEventArgs e)
{
IEnumerable&lt;IAccount&gt; accounts = await _authenticationClient.GetAccountsAsync();
while (accounts.Any())
{
await _authenticationClient.RemoveAsync(accounts.First());
accounts = await _authenticationClient.GetAccountsAsync();
}
VisualStateManager.GoToState(this, UnauthenticatedStateName, true);
}
public string GivenName
{
get { return (string)GetValue(GivenNameProperty); }
set { SetValue(GivenNameProperty, value); }
}
}
}
</code></pre>
<p>There's a lot here so lets break it down:</p>
<h4 id="publicclientapplicationbuilder">PublicClientApplicationBuilder</h4>
<pre><code class="language-c#">_authenticationClient = PublicClientApplicationBuilder
.Create(Authentication.ClientId)
#if __IOS__
.WithIosKeychainSecurityGroup(Authentication.BundleName)
#endif
.WithB2CAuthority(Authentication.Authority)
.WithRedirectUri(Authentication.RedirectUri)
.WithUnoHelpers()
.Build();
</code></pre>
<p>The <code>PublicClientApplicationBuilder</code> class is used to configure and build a <code>PublicClientApplication</code> instance. This class is used to:</p>
<blockquote class="blockquote">
<p>acquire tokens in desktop or mobile applications (Desktop / UWP / Xamarin.iOS / Xamarin.Android). Public client applications are not trusted to safely keep application secrets, and therefore they only access Web APIs in the name of the user only</p>
</blockquote>
<p>To rephrase, because apps that get installed on desktop or mobile devices can be relatively easily decompiled, they can't effectively keep secrets in the way apps that run on a remote machine (i.e. server rendered web-apps) can. As such, this class is able to invoke an authentication flow using only a ClientId and RedirectUri which, while sensitive, do not directly grant the app any authentication rights and are therefore not considered secret. Equally, while this application is running in a browser (via WASM) as an SPA, we need to ensure no secrets are held in the JavaScript VM instance as these can also be retrieved by malicious actors.</p>
<p>To build a <code>PublicClientApplication</code> instance we need to provide the <code>PublicClientApplicationBuilder</code> with the ClientId, Authority and RedirectUri values we encountered while <a href="http://localhost:5080/posts/UnoB2C#step-2-creating-a-tenant">creating our tenant</a>. For iOS we also need to provide the 'IosKeychainSecurityGroup' value which we enclose in a compiler directive so that it is only used on that platform. We provide these values via the <code>Authentication</code> class which will read the values from <code>Authentication.Secret.cs</code>.</p>
<p>Of particular note here is the <code>.WithUnoHelpers()</code> line. This extension method provides a custom implementation of <a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.extensibility.icustomwebui?view=azure-dotnet"><code>ICustomWebUI</code></a> and <a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.imsalhttpclientfactory?view=azure-dotnet"><code>IMsalHttpClientFactory</code></a> to MSAL.NET which allows it to perform authentication in WASM in <em>exactly the same way</em> as it would for an app running on a desktop or mobile device. This is just fantastic and both the MSAL.NET team and Uno Platform deserve kudos for creating and exploiting hooks that allow this use-case to function with so little friction.</p>
<h4 id="acquiretokensilent">AcquireTokenSilent</h4>
<pre><code class="language-c#">var accounts = await _authenticationClient.GetAccountsAsync();
var result = await _authenticationClient
.AcquireTokenSilent(Authentication.Scopes, accounts.FirstOrDefault())
.ExecuteAsync();
</code></pre>
<p>When a user successfully authenticates with Azure AD B2C, they are provided both an <a href="https://auth0.com/docs/tokens/access-tokens">access token</a> and a <a href="https://auth0.com/docs/tokens/refresh-tokens">refresh token</a>. Both these tokens are stored in a local cache associated with the application. These cached tokens can be used across/between sessions to ensure a user isn't constantly being prompted to authenticate with a service.</p>
<p>As such, the first thing we endeavour to do when starting the authentication process is to check to see if there is a valid access token or refresh token (which will be automatically exchanged for a new access token) in the local cache. If there is, then the user has already authenticated and we should use the current tokens to avoid prompting the user to authenticate a second time.</p>
<p>And this is what <code>AcquireTokenSilent</code> silent does. We first get a list of accounts in the token cache and (for simplicity) use the first account we find to check for the presence of a valid token. If one is found, authentication succeeds and no further action is required. If a valid token is not found, then the <code>MsalUiRequiredException</code> is thrown which we handle to perform authentication interactively.</p>
<h4 id="acquiretokeninteractive">AcquireTokenInteractive</h4>
<pre><code class="language-c#">var result = await _authenticationClient
.AcquireTokenInteractive(Authentication.Scopes)
.WithPrompt(Prompt.ForceLogin)
.WithUnoHelpers()
.ExecuteAsync();
</code></pre>
<p>If a cached token was not available, we need to prompt the user to authenticate using an interactive process. This process involves opening a browser window and navigating to the authentication page for your Azure AD B2C tenant. Once authentication is complete, an authorization code is returned via the RedirectUri that MSAL is able to exchange for access and refresh tokens which are then stored in the local cache.</p>
<p>Again, note the call to <code>.WithUnoHelpers()</code>. This call performs platform dependent set-up such that the browser/device is able to correctly display a browser and return to the calling application once authentication is complete.</p>
<p>Finally, you may be wondering about the <code>.WithPrompt(Prompt.ForceLogin)</code>. Well, currently MSAL.NET doesn't <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/589">support a unified means to &quot;sign out&quot; of an account</a>. While you are able to remove cached tokens (see the &quot;sign out&quot; code below) it doesn't clear cookies in the browser used to sign in to an account. This would result in a subsequent call to <code>AcquireTokenInteractive</code> simply logging the user in to the previously used account without prompting them for credentials. To prevent this the <code>.WithPrompt(Prompt.ForceLogin)</code> line is used to ensure the user is prompted for credentials regardless of cookie state.</p>
<h4 id="transitiontoauthenticated">TransitionToAuthenticated</h4>
<pre><code class="language-c#">private void TransitionToAuthenticated(AuthenticationResult authResult)
{
var token = new JwtSecurityToken(authResult.IdToken);
GivenName = token.Claims
.Where(claim =&gt; Authentication.GivenNameClaimType.Equals(claim.Type))
.Select(claim =&gt; claim.Value)
.First();
VisualStateManager.GoToState(this, AuthenticatedStateName, true);
}
</code></pre>
<p>Once a user has been authenticated (either silently or interactively) we transition to the <code>Authenticated</code> state. Before doing so however, we use the access token returned from the authentication process to determine the name of the person who authenticated. As Azure AD B2C returns the access token as a <a href="https://auth0.com/docs/tokens/json-web-tokens">JSON Web Token (JWT)</a> we use the <code>JwtSecurityToken</code> class from the <code>System.IdentityModel.Tokens.Jwt</code> package to parse the token. The token will contain many claims many determined by the registration attributes and tokens claims <a href="#newuserflowclaims">we selected while setting up the policy for our Azure AD B2C tenant</a>.</p>
<p>In this instance, we're interested in the <code>given_name</code> claim so we enumerate through the claims and set the <code>GivenName</code> property to the value of the first claim of this type.</p>
<p>Finally we use the <code>VisualStateManager</code> to transition the UI to the <code>Authenticated</code> state which will greet the user by name.</p>
<h4 id="sign-out">Sign Out</h4>
<pre><code class="language-c#">IEnumerable&lt;IAccount&gt; accounts = await _authenticationClient.GetAccountsAsync();
while (accounts.Any())
{
await _authenticationClient.RemoveAsync(accounts.First());
accounts = await _authenticationClient.GetAccountsAsync();
}
</code></pre>
<p>If we're able to sign-in then we need to be able to sign-out. Unfortunately this process is not quite a slick as the fluent, async methods we used for sign-in and, as described above, doesn't do anything to remove browser cookies which can be used to transparently re-authenticate. This does seem to be the subject of <a href="https://stackoverflow.com/questions/47517434/how-to-sign-out-from-azure-ad-2-0-msal-in-a-desktop-application">much</a> <a href="https://stackoverflow.com/questions/37792244/logout-does-not-work-when-using-microsoft-authentication-library-msal">confusion</a> on both StackOverflow and Github where many of the <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/589">associated</a> <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/425">issues</a> have been closed without a satisfactory solution. Any mention of improving the sign-out experience even seems to have disappeared from the <a href="https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/projects/1">MSAL.NET project boards</a>.</p>
<p>Still, the <code>.WithPrompt(Prompt.ForceLogin)</code> workaround resolves the primary issue for now so we're able to just rely on the code above to remove cached tokens.</p>
<h3 id="android-changes">Android Changes</h3>
<p>In order for authentication to succeed on Android we need to modify both <code>AndroidManifest.xml</code> and the <code>MainActivity.cs</code></p>
<h4 id="androidmanifest.xml">AndroidManifest.xml</h4>
<p>In the 'UnoAuth.Droid' project, expand 'Properties' to show the &quot;AndroidManifest.xml&quot; file. Double-click this file to edit it such that it looks similar to the following:</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; package=&quot;UnoAuth&quot; android:versionCode=&quot;1&quot; android:versionName=&quot;1.0&quot;&gt;
&lt;uses-sdk android:minSdkVersion=&quot;16&quot; android:targetSdkVersion=&quot;29&quot; /&gt;
&lt;application android:label=&quot;UnoAuth&quot;&gt;
&lt;activity android:name=&quot;microsoft.identity.client.BrowserTabActivity&quot;&gt;
&lt;intent-filter&gt;
&lt;action android:name=&quot;android.intent.action.VIEW&quot; /&gt;
&lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
&lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&gt;
&lt;data android:scheme=&quot;msal[ClientId]&quot; android:host=&quot;auth&quot; /&gt;
&lt;/intent-filter&gt;
&lt;/activity&gt;
&lt;/application&gt;
&lt;/manifest&gt;
</code></pre>
<p>Make sure you amend the <code>android:scheme</code> value to use the ClientId from your App Registration then save changes and close the file.</p>
<h4 id="mainactivity">MainActivity</h4>
<p>Open the 'MainActivity.cs' file and amend it to include the following:</p>
<pre><code class="language-c#">using Android.App;
using Android.Content;
using Android.Views;
using Microsoft.Identity.Client;
namespace UnoAuth.Droid
{
[Activity(
MainLauncher = true,
ConfigurationChanges = global::Uno.UI.ActivityHelper.AllConfigChanges,
WindowSoftInputMode = SoftInput.AdjustPan | SoftInput.StateHidden)]
public class MainActivity : Windows.UI.Xaml.ApplicationActivity
{
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data);
}
}
}
</code></pre>
<h3 id="ios-changes">iOS Changes</h3>
<p>As with most everything on iOS, the changes to make authentication work are a little more tricky. We need to change the iOS project properties and both the 'Info.plist' and 'Entitlements.plist' files.</p>
<h4 id="project-properties">Project Properties</h4>
<p>Right click on the iOS project properties, navigate to &quot;iOS Bundle Signing&quot; and select &quot;Manual Provisioning&quot;. Next, under Additional Resources, make sure the Custom Entitlements setting is set to &quot;Entitlements.plist&quot;.</p>
<p>Your iOS Bundle Signing page should now look like this:</p>
<p><a data-fancybox="iosbundlesigning" href="/Content/UnoB2C/iOS Bundle Signing.png"><img src="/Content/UnoB2C/iOS Bundle Signing.png" class="img-responsive" style="margin: auto; max-width:66%; margin-top: 6px; margin-bottom: 6px;" alt="iOS Bundle Signing"/></a></p>
<h4 id="info.plist">Info.plist</h4>
<p>Right click on the 'Info.plist' file in the iOS project and select <code>View Code</code>. At the end of the root <code>&lt;dict&gt;</code> element add the <code>CFBundleURLTypes</code> key and value shown below (amending the <code>CFBundleURLSchemes</code> value to use the ClientId for your app registration):</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
&lt;key&gt;CFBundleDisplayName&lt;/key&gt;
&lt;string&gt;UnoAuth&lt;/string&gt;
&lt;key&gt;CFBundleIdentifier&lt;/key&gt;
&lt;string&gt;com.companyname.UnoAuth&lt;/string&gt;
...
&lt;key&gt;CFBundleURLTypes&lt;/key&gt;
&lt;array&gt;
&lt;dict&gt;
&lt;key&gt;CFBundleURLName&lt;/key&gt;
&lt;string&gt;MSAL&lt;/string&gt;
&lt;key&gt;CFBundleURLSchemes&lt;/key&gt;
&lt;array&gt;
&lt;string&gt;msal[ClientID]&lt;/string&gt;
&lt;/array&gt;
&lt;key&gt;CFBundleTypeRole&lt;/key&gt;
&lt;string&gt;None&lt;/string&gt;
&lt;/dict&gt;
&lt;/array&gt;
&lt;/dict&gt;
&lt;/plist&gt;
</code></pre>
<p>Finally copy the <code>CFBundleIdentifier</code> value (in this case <code>com.companyname.UnoAuth</code>) then save and close the file.</p>
<h4 id="entitlements.plist">Entitlements.plist</h4>
<p>Double click on the 'Entitlements.plist' file in the iOS project to open the visual editor. In the 'Entitlements' list select &quot;Keychain&quot; and then tick &quot;Enable Keychain&quot; in the 'Description' section. Finally, paste the bundle identifier you copied from 'Info.plist' into the 'Keychain Groups' text box so it looks like this:</p>
<p><a data-fancybox="entitlementsplist" href="/Content/UnoB2C/Entitlements plist.png"><img src="/Content/UnoB2C/Entitlements plist.png" class="img-responsive" style="margin: auto; max-width:66%; margin-top: 6px; margin-bottom: 6px;" alt="Entitlements plist"/></a></p>
<p>Finally save the changes and close the file.</p>
<h2 id="testing">Testing</h2>
<p>Now, if everything is set up correctly, you should be able to use Azure AD B2C and MSAL.NET to authenticate users. Here is UnoAuth running on...</p>
<h3 id="uwp">UWP</h3>
<video class="img-responsive" style="margin: auto; width:80%; margin-top: 6px; margin-bottom: 6px;" controls>
<source src="/Content/UnoB2C/UWP Authentication.mp4" type="video/mp4"/>
Your browser does not support the video tag
</video>
<h3 id="wasm">WASM</h3>
<video class="img-responsive" style="margin: auto; width:80%; margin-top: 6px; margin-bottom: 6px;" controls>
<source src="/Content/UnoB2C/WASM Authentication.mp4" type="video/mp4"/>
Your browser does not support the video tag
</video>
<h3 id="android">Android</h3>
<video class="img-responsive" style="margin: auto; width:40%; margin-top: 6px; margin-bottom: 6px;" controls>
<source src="/Content/UnoB2C/Droid Authentication.mp4" type="video/mp4"/>
Your browser does not support the video tag
</video>
<h3 id="ios">iOS</h3>
<video class="img-responsive" style="margin: auto; width:40%; margin-top: 6px; margin-bottom: 6px;" controls>
<source src="/Content/UnoB2C/iOS Authentication.mp4" type="video/mp4"/>
Your browser does not support the video tag
</video>
<h2 id="conclusion">Conclusion</h2>
<p>As we can see, it is now possible to use Azure AD B2C and MSAL.NET to perform client-side authentication, across multiple platforms, using a single code-base. Furthermore, while a few platform specific tweaks are required in a couple of the head projects, the code to perform authentication is both concise, understandable and shared by all platforms.</p>
<p>While IAM remains a complicated subject (as attested to by the length of this post!) I hope the above provides sufficient information that a reader is able to quickly get these technologies working together and allow them to move on to more engaging parts of their app.</p>
<h2 id="finally">Finally</h2>
<p>If you're interested in using the Uno Platform to deliver cross-platform apps or have an upcoming project for which you'd like evaluate Uno Platform's fit, then please feel free to drop me a line using any of the links below or from my <a href="https://ian.bebbs.co.uk/about">about page</a>. As a freelance software developer and remote contractor I'm always interested in hearing from potential new clients or ideas for new collaborations.</p>
</content:encoded>
</item>
<item>
<title>A Blogging Milestone</title>
<link>http://ian.bebbs.co.uk/posts/BlogMilestone</link>
<description><p>Today my blog hit a minor milestone: over 12,000 page views in the last 365 days. That's over two-thousand page views a month! While a long way short of other notable tech bloggers (yes, I'm looking at you <a href="https://www.hanselman.com/blog/">Hanselman</a>), I think it's a pretty decent number, particularly when considering the somewhat limited audience for my very targeted content. In this post I provide insights into my "Top 20" posts and my plans for the coming months.</p></description>
<enclosure url="http://ian.bebbs.co.uk/Content/BlogMilestone/Weighted%20Posts%20-%20Background.png" length="0" type="image" />
<guid>http://ian.bebbs.co.uk/posts/BlogMilestone</guid>
<pubDate>Fri, 04 Sep 2020 00:00:00 GMT</pubDate>
<content:encoded><h2 id="tldr">TL;DR</h2>
<p>Today my blog hit a minor milestone: over 12,000 page views in the last 365 days. That's over two-thousand page views a month! While a long way short of other notable tech bloggers (yes, I'm looking at you <a href="https://www.hanselman.com/blog/">Hanselman</a>), I think it's a pretty decent number, particularly when considering the somewhat limited audience for my very targeted content. In this post I provide insights into my &quot;Top 20&quot; posts and my plans for the coming months.</p>
<h2 id="top-20">Top 20</h2>
<h3 id="posts-by-weighted-page-view">Posts By Weighted Page View</h3>
<p>While looking over the blog statistics for the past year, I was very interested to understand which were my &quot;top&quot; posts. Initially I thought total page views for each post would be a good metric but, given older posts will naturally have more hits, I decided to weight total page views by publication date. This gave me the following:</p>
<img src="/Content/BlogMilestone/Weighted Posts.png" class="img-responsive" style="margin: auto; max-width:90%; margin-top: 6px; margin-bottom: 6px;" alt="Weighted Posts"/>
<p>In this chart the outer ring is the Top 20 blog posts based on weighted total page views and the inner ring is the same 20 blog posts based on actual total page views.</p>
<p>The Top 20 are as follows:</p>
<table class="table">
<thead>
<tr>
<th>Post</th>
<th style="text-align: right;">Weighted Views</th>
<th style="text-align: right;">Total Views</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/UnoValue">On the incredible value proposition of .NET &amp; the Uno Platform</a></td>
<td style="text-align: right;">152.19</td>
<td style="text-align: right;">739</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/LessReSTMoreHotChocolate">Less ReST, more Hot Chocolate</a></td>
<td style="text-align: right;">112.25</td>
<td style="text-align: right;">1741</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/UnoLinux">Running UWP on Linux With Uno</a></td>
<td style="text-align: right;">100.57</td>
<td style="text-align: right;">445</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/UnoPi">Running UWP on a Raspberry Pi Using Uno Platform</a></td>
<td style="text-align: right;">88.17</td>
<td style="text-align: right;">348</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/MLinUWP">State-of-the-art ML in UWP</a></td>
<td style="text-align: right;">87.87</td>
<td style="text-align: right;">299</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/BuildingDotNetCore3WithAzurePipelines">Building .NET Core 3.0 With Azure Pipelines</a></td>
<td style="text-align: right;">62.03</td>
<td style="text-align: right;">1138</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/UnoChat">Cross-Platform Real-Time Communication with Uno &amp; SignalR</a></td>
<td style="text-align: right;">56.38</td>
<td style="text-align: right;">401</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/Uno">The Seven GUIs of Christmas</a></td>
<td style="text-align: right;">53.81</td>
<td style="text-align: right;">1028</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/AugmentingTheGenericHost">Augmenting the .NET Core 3.0 Generic Host</a></td>
<td style="text-align: right;">47.2</td>
<td style="text-align: right;">806</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/COduo-Part4">Many platforms, one world - Part 4</a></td>
<td style="text-align: right;">31.36</td>
<td style="text-align: right;">340</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/LightweightRuntimeCompositionForGenericHost">Light-weight run-time composition for the .NET Core 3.0 Generic Host</a></td>
<td style="text-align: right;">28.24</td>
<td style="text-align: right;">488</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/UnoWasmDocker">Uno WebAssembly Containerization</a></td>
<td style="text-align: right;">25.47</td>
<td style="text-align: right;">220</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/NetworkBootingManyRaspberryPis">Network Booting Many Raspberry Pis</a></td>
<td style="text-align: right;">22.61</td>
<td style="text-align: right;">355</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/COduo-Part1">Many platforms, one world - Part 1</a></td>
<td style="text-align: right;">19.54</td>
<td style="text-align: right;">230</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/COduo-Part3">Many platforms, one world - Part 3</a></td>
<td style="text-align: right;">12.12</td>
<td style="text-align: right;">138</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/ReactiveStateMachines">Reactive State Machines</a></td>
<td style="text-align: right;">11.83</td>
<td style="text-align: right;">442</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/UnoWithSwagger">Giving Uno Some Swagger</a></td>
<td style="text-align: right;">11.63</td>
<td style="text-align: right;">105</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/UsingHyperlinkInMVVM">Using a Hyperlink in MVVM</a></td>
<td style="text-align: right;">11.44</td>
<td style="text-align: right;">440</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/COduo-Part2">Many platforms, one world - Part 2</a></td>
<td style="text-align: right;">10.69</td>
<td style="text-align: right;">124</td>
</tr>
<tr>
<td><a href="https://ian.bebbs.co.uk/posts/Codewars">A Kata for Katas</a></td>
<td style="text-align: right;">10.47</td>
<td style="text-align: right;">98</td>