├── active-directory-b2c-wpf
├── Properties
│ ├── Settings.settings
│ ├── Settings.Designer.cs
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ └── Resources.resx
├── App.xaml
├── TokenCacheHelper.cs
├── MainWindow.xaml
├── App.xaml.cs
├── active-directory-b2c-wpf.csproj
└── MainWindow.xaml.cs
├── active-directory-b2c-wpf.sln
├── LICENSE
├── .gitattributes
├── .gitignore
└── README.md
/active-directory-b2c-wpf/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26403.7
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "active-directory-b2c-wpf", "active-directory-b2c-wpf\active-directory-b2c-wpf.csproj", "{9A4269AF-7862-482C-A9FC-98D3542C4129}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {9A4269AF-7862-482C-A9FC-98D3542C4129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {9A4269AF-7862-482C-A9FC-98D3542C4129}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {9A4269AF-7862-482C-A9FC-98D3542C4129}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {9A4269AF-7862-482C-A9FC-98D3542C4129}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace active_directory_b2c_wpf.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.6.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/TokenCacheHelper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.IO;
5 | using System.Security.Cryptography;
6 | using Microsoft.Identity.Client;
7 |
8 | namespace active_directory_b2c_wpf
9 | {
10 | static class TokenCacheHelper
11 | {
12 | ///
13 | /// Path to the token cache
14 | ///
15 | public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin";
16 |
17 | private static readonly object FileLock = new object();
18 |
19 | public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
20 | {
21 | lock (FileLock)
22 | {
23 | args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
24 | ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
25 | null,
26 | DataProtectionScope.CurrentUser)
27 | : null);
28 | }
29 | }
30 |
31 | public static void AfterAccessNotification(TokenCacheNotificationArgs args)
32 | {
33 | // if the access operation resulted in a cache update
34 | if (args.HasStateChanged)
35 | {
36 | lock (FileLock)
37 | {
38 | // reflect changes in the persistent store
39 | File.WriteAllBytes(CacheFilePath,
40 | ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
41 | null,
42 | DataProtectionScope.CurrentUser));
43 | }
44 | }
45 | }
46 |
47 | internal static void Bind(ITokenCache tokenCache)
48 | {
49 | tokenCache.SetBeforeAccess(BeforeAccessNotification);
50 | tokenCache.SetAfterAccess(AfterAccessNotification);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("active_directory_b2c_wpf")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("active_directory_b2c_wpf")]
15 | [assembly: AssemblyCopyright("Copyright © 2017")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("1.0.0.0")]
55 | [assembly: AssemblyFileVersion("1.0.0.0")]
56 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace active_directory_b2c_wpf.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("active_directory_b2c_wpf.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/App.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.IO;
6 | using System.Text;
7 | using System.Windows;
8 | using Microsoft.Identity.Client;
9 | using Microsoft.Identity.Client.Desktop;
10 |
11 | namespace active_directory_b2c_wpf
12 | {
13 | ///
14 | /// Interaction logic for App.xaml
15 | ///
16 | public partial class App : Application
17 | {
18 | ///
19 | /// B2C tenant name
20 | ///
21 | private static readonly string TenantName = "fabrikamb2c";
22 | private static readonly string Tenant = $"{TenantName}.onmicrosoft.com";
23 | private static readonly string AzureAdB2CHostname = $"{TenantName}.b2clogin.com";
24 |
25 | ///
26 | /// ClientId for the application which initiates the login functionality (this app)
27 | ///
28 | private static readonly string ClientId = "Enter_the_Application_Id_Here";
29 |
30 | ///
31 | /// Should be one of the choices on the Azure AD B2c / [This App] / Authentication blade
32 | ///
33 | private static readonly string RedirectUri = $"https://{TenantName}.b2clogin.com/oauth2/nativeclient";
34 |
35 | ///
36 | /// From Azure AD B2C / UserFlows blade
37 | ///
38 | public static string PolicySignUpSignIn = "b2c_1_susi";
39 | public static string PolicyEditProfile = "b2c_1_edit_profile";
40 | public static string PolicyResetPassword = "b2c_1_reset";
41 |
42 | ///
43 | /// Note: AcquireTokenInteractive will fail to get the AccessToken if "Admin Consent" has not been granted to this scope. To achieve this:
44 | ///
45 | /// 1st: Azure AD B2C / App registrations / [API App] / Expose an API / Add a scope
46 | /// 2nd: Azure AD B2C / App registrations / [This App] / API Permissions / Add a permission / My APIs / [API App] / Select & Add Permissions
47 | /// 3rd: Azure AD B2C / App registrations / [This App] / API Permissions / ... (next to add a permission) / Grant Admin Consent for [tenant]
48 | ///
49 | public static string[] ApiScopes = { $"https://{Tenant}/helloapi/demo.read" };
50 |
51 | ///
52 | /// URL for API which will receive the bearer token corresponding to this authentication
53 | ///
54 | public static string ApiEndpoint = "https://fabrikamb2chello.azurewebsites.net/hello";
55 |
56 | // Shouldn't need to change these:
57 | private static string AuthorityBase = $"https://{AzureAdB2CHostname}/tfp/{Tenant}/";
58 | public static string AuthoritySignUpSignIn = $"{AuthorityBase}{PolicySignUpSignIn}";
59 | public static string AuthorityEditProfile = $"{AuthorityBase}{PolicyEditProfile}";
60 | public static string AuthorityResetPassword = $"{AuthorityBase}{PolicyResetPassword}";
61 |
62 | public static IPublicClientApplication PublicClientApp { get; private set; }
63 |
64 | static App()
65 | {
66 | PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
67 | .WithB2CAuthority(AuthoritySignUpSignIn)
68 | .WithRedirectUri(RedirectUri)
69 | .WithWindowsEmbeddedBrowserSupport()
70 | .WithLogging(Log, LogLevel.Info, true)
71 | .Build();
72 |
73 | TokenCacheHelper.Bind(PublicClientApp.UserTokenCache);
74 | }
75 |
76 | private static void Log(LogLevel level, string message, bool containsPii)
77 | {
78 | string logs = $"{level} {message}{Environment.NewLine}";
79 | File.AppendAllText(System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalLogs.txt", logs);
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/active-directory-b2c-wpf.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {9A4269AF-7862-482C-A9FC-98D3542C4129}
8 | WinExe
9 | active_directory_b2c_wpf
10 | active_directory_b2c_wpf
11 | v4.8
12 | 512
13 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
14 | 4
15 | true
16 |
17 |
18 |
19 |
20 |
21 | AnyCPU
22 | true
23 | full
24 | false
25 | bin\Debug\
26 | DEBUG;TRACE
27 | prompt
28 | 4
29 |
30 |
31 | AnyCPU
32 | pdbonly
33 | true
34 | bin\Release\
35 | TRACE
36 | prompt
37 | 4
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 4.0
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | MSBuild:Compile
62 | Designer
63 |
64 |
65 |
66 | MSBuild:Compile
67 | Designer
68 |
69 |
70 | App.xaml
71 | Code
72 |
73 |
74 | MainWindow.xaml
75 | Code
76 |
77 |
78 |
79 |
80 | Code
81 |
82 |
83 | True
84 | True
85 | Resources.resx
86 |
87 |
88 | True
89 | Settings.settings
90 | True
91 |
92 |
93 | ResXFileCodeGenerator
94 | Resources.Designer.cs
95 |
96 |
97 | SettingsSingleFileGenerator
98 | Settings.Designer.cs
99 |
100 |
101 |
102 |
103 | 4.54.0
104 |
105 |
106 | 4.54.0
107 |
108 |
109 | 1.0.1777-prerelease
110 |
111 |
112 | 13.0.1
113 |
114 |
115 | 4.3.2
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 | *.VC.VC.opendb
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | # TODO: Comment the next line if you want to checkin your web deploy settings
144 | # but database connection strings (with potential passwords) will be unencrypted
145 | *.pubxml
146 | *.publishproj
147 |
148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
149 | # checkin your Azure Web App publish settings, but sensitive information contained
150 | # in these scripts will be unencrypted
151 | PublishScripts/
152 |
153 | # NuGet Packages
154 | *.nupkg
155 | # The packages folder can be ignored because of Package Restore
156 | **/packages/*
157 | # except build/, which is used as an MSBuild target.
158 | !**/packages/build/
159 | # Uncomment if necessary however generally it will be regenerated when needed
160 | #!**/packages/repositories.config
161 | # NuGet v3's project.json files produces more ignoreable files
162 | *.nuget.props
163 | *.nuget.targets
164 |
165 | # Microsoft Azure Build Output
166 | csx/
167 | *.build.csdef
168 |
169 | # Microsoft Azure Emulator
170 | ecf/
171 | rcf/
172 |
173 | # Windows Store app package directories and files
174 | AppPackages/
175 | BundleArtifacts/
176 | Package.StoreAssociation.xml
177 | _pkginfo.txt
178 |
179 | # Visual Studio cache files
180 | # files ending in .cache can be ignored
181 | *.[Cc]ache
182 | # but keep track of directories ending in .cache
183 | !*.[Cc]ache/
184 |
185 | # Others
186 | ClientBin/
187 | ~$*
188 | *~
189 | *.dbmdl
190 | *.dbproj.schemaview
191 | *.pfx
192 | *.publishsettings
193 | node_modules/
194 | orleans.codegen.cs
195 |
196 | # Since there are multiple workflows, uncomment next line to ignore bower_components
197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
198 | #bower_components/
199 |
200 | # RIA/Silverlight projects
201 | Generated_Code/
202 |
203 | # Backup & report files from converting an old project file
204 | # to a newer Visual Studio version. Backup files are not needed,
205 | # because we have git ;-)
206 | _UpgradeReport_Files/
207 | Backup*/
208 | UpgradeLog*.XML
209 | UpgradeLog*.htm
210 |
211 | # SQL Server files
212 | *.mdf
213 | *.ldf
214 |
215 | # Business Intelligence projects
216 | *.rdl.data
217 | *.bim.layout
218 | *.bim_*.settings
219 |
220 | # Microsoft Fakes
221 | FakesAssemblies/
222 |
223 | # GhostDoc plugin setting file
224 | *.GhostDoc.xml
225 |
226 | # Node.js Tools for Visual Studio
227 | .ntvs_analysis.dat
228 |
229 | # Visual Studio 6 build log
230 | *.plg
231 |
232 | # Visual Studio 6 workspace options file
233 | *.opt
234 |
235 | # Visual Studio LightSwitch build output
236 | **/*.HTMLClient/GeneratedArtifacts
237 | **/*.DesktopClient/GeneratedArtifacts
238 | **/*.DesktopClient/ModelManifest.xml
239 | **/*.Server/GeneratedArtifacts
240 | **/*.Server/ModelManifest.xml
241 | _Pvt_Extensions
242 |
243 | # Paket dependency manager
244 | .paket/paket.exe
245 | paket-files/
246 |
247 | # FAKE - F# Make
248 | .fake/
249 |
250 | # JetBrains Rider
251 | .idea/
252 | *.sln.iml
253 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | page_type: sample
3 | languages:
4 | - csharp
5 | products:
6 | - azure
7 | description: "This simple sample demonstrates how to use the Microsoft Authentication Library (MSAL) for .NET to get an access token and call an API secured by Azure AD B2C."
8 | urlFragment: active-directory-b2c-dotnet-desktop
9 | ---
10 |
11 | # WPF application signing in users with Azure Active Directory B2C and calling an API
12 |
13 | > This branch is using MSAL.NET 4.x. If you are interested in a previous version of the sample using
14 | > MSAL.NET 2.x, go to the [master](https://github.com/Azure-Samples/active-directory-b2c-dotnet-desktop/tree/master) branch
15 |
16 |
17 | This simple sample demonstrates how to use the [Microsoft Authentication Library (MSAL) for .NET](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet) to get an access token and call an API secured by Azure AD B2C.
18 |
19 | ## How To Run This Sample
20 |
21 | There are two ways to run this sample:
22 |
23 | 1. **Using the demo environment** - The sample is already configured to use a demo environment and can be run simply by downloading this repository and running the app on your machine. See steps below for Running with demo environment.
24 | 2. **Using your own Azure AD B2C tenant** - If you would like to use your own Azure AD B2C configuration, follow the steps listed below for Using your own Azure AD B2C tenant. Please note, the api call will only work with domains using `{tenantName}.b2clogin.com`, as the node.js api used for the api call has been updated to handle `b2clogin.com` and not `login.microsoftonline.com`. If using `login.microsoftonline.com` or a custom b2c domain, you will need to host your own web api (see step 3 below), otherwise, you will see "authorized" when making the api call with this sample as-is.
25 |
26 | ## Using the demo environment
27 |
28 | This sample demonstrates how to sign in or sign up for an account at "Wingtip Toys" (the demo environment for this sample) using a WPF Desktop application.
29 |
30 | Once signed-in, clicking on the **Call API** button shows the display name you used when you created your account. The **Edit Profile** button allows you to change your display name and city. The **Logout** button logs you out of the application.
31 |
32 | ### Step 1: Clone or download this repository
33 |
34 | From your shell or command line:
35 |
36 | ```
37 | git clone https://github.com/Azure-Samples/active-directory-b2c-dotnet-desktop.git
38 | ```
39 |
40 | ### Step 2: Run the project
41 |
42 | Open the `active-directory-b2c-wpf.sln` and run the project.
43 |
44 | The sample demonstrates the following functionality:
45 |
46 | 1. Click the sign-in button at the top of the application screen. The sample works exactly in the same way regardless of the account type you choose, apart from some visual differences in the authentication and consent experience. Upon successful sign in, the application screen will list some basic profile info for the authenticated user and show buttons that allow you to edit your profile, call an API and sign out.
47 | 2. Close the application and reopen it. You will see that the app retains access to the API and retrieves the user info right away, without the need to sign in again.
48 | 3. Sign out by clicking the Sign out button and confirm that you lose access to the API until the exit interactive sign in.
49 |
50 |
51 | ## Using your own Azure AD B2C Tenant
52 |
53 | In the previous section, you learned how to run the sample application using the demo environment. In this section, you'll learn how to configure this WPF application and a related [Node.js Web API with Azure AD B2C sample](https://github.com/Azure-Samples/active-directory-b2c-javascript-nodejs-webapi) to work with your own Azure AD B2C Tenant.
54 |
55 | ### Step 1: Get your own Azure AD B2C tenant
56 |
57 | First, you'll need an Azure AD B2C tenant. If you don't have an existing Azure AD B2C tenant that you can use for testing purposes, you can create your own by following [these instruction](https://azure.microsoft.com/documentation/articles/active-directory-b2c-get-started/).
58 |
59 | ### Step 2: Create your own policies
60 |
61 | This sample uses three types of policies: a unified sign-up/sign-in policy, a profile editing policy, and a password reset policy. Create one policy of each type by following [the built-in policy instructions](https://azure.microsoft.com/documentation/articles/active-directory-b2c-reference-policies). You may choose to include as many or as few identity providers as you wish.
62 |
63 | If you already have existing policies in your Azure AD B2C tenant, feel free to re-use those policies in this sample.
64 |
65 | ### Step 3: Register your own Web API with Azure AD B2C
66 |
67 | As you saw in the demo environment, this sample calls a Web API at https://fabrikamb2chello.azurewebsites.net. This demo Web API uses the same code found in the sample [Node.js Web API with Azure AD B2C](https://github.com/Azure-Samples/active-directory-b2c-javascript-nodejs-webapi), in case you need to reference it for debugging purposes.
68 |
69 | You must replace the demo environment Web API with your own Web API. If you do not have your own Web API, you can clone the [Node.js Web API with Azure AD B2C](https://github.com/Azure-Samples/active-directory-b2c-javascript-nodejs-webapi) sample and register it with your tenant.
70 |
71 | #### How to setup and register the Node.js Web API sample
72 |
73 | First, clone the Node.js Web API sample repository into its own directory, for example:
74 |
75 | ```
76 | cd ..
77 | git clone https://github.com/Azure-Samples/active-directory-b2c-javascript-nodejs-webapi.git
78 | ```
79 |
80 | Second, follow the instructions at [register a Web API with Azure AD B2C](https://docs.microsoft.com/azure/active-directory-b2c/active-directory-b2c-app-registration#register-a-web-api) to register the Node.js Web API sample with your tenant. Registering your Web API allows you to define the scopes that your single page application will request access tokens for.
81 |
82 | Provide the following values for the Node.js Web API registration:
83 |
84 | - Provide a descriptive Name for the Node.js Web API, for example, `My Test Node.js Web API`. You will identify this application by its Name whenever working in the Azure portal.
85 | - Mark **Yes** for the **Web App/Web API** setting for your application.
86 | - Set the **Reply URL** to `http://localhost:5000`. This is the port number that the Node.js Web API sample is configured to run on.
87 | - Set the **AppID URI** to `demoapi`. This AppID URI is a unique identifier representing this Node.jS Web API. The AppID URI is used to construct the scopes that are configured in you single page application's code. For example, in this Node.js Web API sample, the scope will have the value `https://.onmicrosoft.com/demoapi/demo.read`
88 | - Create the application.
89 | - Once the application is created, open your `My Test Node.js Web API` application and then open the **Published Scopes** window (in the left nav menu) and add the scope `demo.read` followed by a description `demoing a read scenario`. Click **Save**.
90 |
91 | Third, in the `index.html` file of the Node.js Web API sample, update the following variables to refer to your Web API registration.
92 |
93 | ```
94 | var tenantID = ".onmicrosoft.com";
95 | var clientID = "";
96 | var policyName = "";
97 | ```
98 |
99 | Lastly, to run your Node.js Web API, run the following command from your shell or command line
100 |
101 | ```
102 | npm install && npm update
103 | node index.js
104 | ```
105 |
106 | Your Node.js Web API sample is now running on Port 5000.
107 |
108 |
109 | ### Step 4: Register your Native app
110 |
111 | Now you need to [register your native app in your B2C tenant](https://docs.microsoft.com/azure/active-directory-b2c/add-native-application?tabs=app-reg-ga), so that it has its own Application ID.
112 |
113 | Your native application registration should include the following information:
114 |
115 | - Provide a descriptive Name for the single page application, for example, `My Test WPF App`. You will identify this application by its Name within the Azure portal.
116 | - Mark **Yes** for the **Native Client** setting for your application.
117 | - Create your application.
118 | - Once the application is created, open your `My Test WPF App` and open the **API Access** window (in the left nav menu). Click **Add** and select the name of the Node.js Web API you registered previously, for example `My Test Node.js Web API`. Select the scope(s) you defined previously, for example, `demo.read` and hit **Save**.
119 |
120 | ### Step 5: Configure your Visual Studio project with your Azure AD B2C app registrations
121 |
122 | 1. Open the solution in Visual Studio.
123 | 1. Open the `App.xaml.cs` file.
124 | 1. Find the assignment for `public static string Tenant` and replace the value with your tenant name.
125 | 1. Find the assignment for `public static string ClientID` and replace the value with the Application ID from your Native app registration, for example `My Test WPF App`.
126 | 1. Find the assignment for each of the policies, for example `public static string PolicySignUpSignIn`, and replace the names of the policies you created in Step 2, e.g. `b2c_1_SiUpIn`
127 | 1. Find the assignment for the scopes `public static string[] ApiScopes` and replace with the scope you created in Step 3, for example, `https://.onmicrosoft.com/demoapi/demo.read`.
128 | 1. Change the `ApiEndpoint` variable to point to your Node.js Web API `hello` endpoint running locally at `"http://localhost:5000/hello"`
129 |
130 | ### Step 6: Run the sample
131 |
132 | 1. Rebuild the solution and run the app.
133 | 2. Click the sign-in button at the top of the application screen. The sample works exactly in the same way regardless of the account type you choose, apart from some visual differences in the authentication and consent experience. Upon successful sign in, the application screen will list some basic profile info for the authenticated user and show buttons that allow you to edit your profile, call an API and sign out.
134 | 3. Close the application and reopen it. You will see that the app retains access to the API and retrieves the user info right away, without the need to sign in again.
135 | 4. Sign out by clicking the Sign out button and confirm that you lose access to the API until the exit interactive sign in.
136 |
137 | ## More information
138 | For more information on Azure B2C, see [the Azure AD B2C documentation homepage](http://aka.ms/aadb2c).
139 |
--------------------------------------------------------------------------------
/active-directory-b2c-wpf/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Microsoft.Identity.Client;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Diagnostics;
8 | using System.Linq;
9 | using System.Net.Http;
10 | using System.Net.Http.Headers;
11 | using System.Threading.Tasks;
12 | using System.Windows;
13 | using System.Windows.Interop;
14 | using Newtonsoft.Json.Linq;
15 | using System.Text;
16 |
17 | namespace active_directory_b2c_wpf
18 | {
19 | ///
20 | /// Interaction logic for MainWindow.xaml
21 | ///
22 | public partial class MainWindow : Window
23 | {
24 | public MainWindow()
25 | {
26 | InitializeComponent();
27 | }
28 |
29 | private async void SignInButton_Click(object sender, RoutedEventArgs e)
30 | {
31 | AuthenticationResult authResult = null;
32 | var app = App.PublicClientApp;
33 | try
34 | {
35 | ResultText.Text = "";
36 | authResult = await app.AcquireTokenInteractive(App.ApiScopes)
37 | .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
38 | .ExecuteAsync();
39 |
40 | DisplayUserInfo(authResult);
41 | UpdateSignInState(true);
42 | }
43 | catch (MsalException ex)
44 | {
45 | try
46 | {
47 | if (ex.Message.Contains("AADB2C90118"))
48 | {
49 | authResult = await app.AcquireTokenInteractive(App.ApiScopes)
50 | .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
51 | .WithPrompt(Prompt.SelectAccount)
52 | .WithB2CAuthority(App.AuthorityResetPassword)
53 | .ExecuteAsync();
54 | }
55 | else
56 | {
57 | ResultText.Text = $"Error Acquiring Token:{Environment.NewLine}{ex}";
58 | }
59 | }
60 | catch (Exception exe)
61 | {
62 | ResultText.Text = $"Error Acquiring Token:{Environment.NewLine}{exe}";
63 | }
64 | }
65 | catch (Exception ex)
66 | {
67 | ResultText.Text = $"Error Acquiring Token:{Environment.NewLine}{ex}";
68 | }
69 |
70 | DisplayUserInfo(authResult);
71 | }
72 |
73 | private async void EditProfileButton_Click(object sender, RoutedEventArgs e)
74 | {
75 | var app = App.PublicClientApp;
76 | try
77 | {
78 | ResultText.Text = $"Calling API:{App.AuthorityEditProfile}";
79 |
80 | AuthenticationResult authResult = await app.AcquireTokenInteractive(App.ApiScopes)
81 | .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
82 | .WithB2CAuthority(App.AuthorityEditProfile)
83 | .WithPrompt(Prompt.NoPrompt)
84 | .ExecuteAsync(new System.Threading.CancellationToken());
85 |
86 | DisplayUserInfo(authResult);
87 | }
88 | catch (Exception ex)
89 | {
90 | ResultText.Text = $"Session has expired, please sign out and back in.{App.AuthorityEditProfile}{Environment.NewLine}{ex}";
91 | }
92 | }
93 |
94 | private async void CallApiButton_Click(object sender, RoutedEventArgs e)
95 | {
96 | AuthenticationResult authResult = null;
97 | var app = App.PublicClientApp;
98 | var accounts = await app.GetAccountsAsync(App.PolicySignUpSignIn);
99 | try
100 | {
101 | authResult = await app.AcquireTokenSilent(App.ApiScopes, accounts.FirstOrDefault())
102 | .ExecuteAsync();
103 | }
104 | catch (MsalUiRequiredException ex)
105 | {
106 | // A MsalUiRequiredException happened on AcquireTokenSilentAsync.
107 | // This indicates you need to call AcquireTokenAsync to acquire a token
108 | Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
109 |
110 | try
111 | {
112 | authResult = await app.AcquireTokenInteractive(App.ApiScopes)
113 | .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
114 | .ExecuteAsync();
115 | }
116 | catch (MsalException msalex)
117 | {
118 | ResultText.Text = $"Error Acquiring Token:{Environment.NewLine}{msalex}";
119 | }
120 | }
121 | catch (Exception ex)
122 | {
123 | ResultText.Text = $"Error Acquiring Token Silently:{Environment.NewLine}{ex}";
124 | return;
125 | }
126 |
127 | if (authResult != null)
128 | {
129 | if (string.IsNullOrEmpty(authResult.AccessToken))
130 | {
131 | ResultText.Text = "Access token is null (could be expired). Please do interactive log-in again." ;
132 | }
133 | else
134 | {
135 | ResultText.Text = await GetHttpContentWithToken(App.ApiEndpoint, authResult.AccessToken);
136 | DisplayUserInfo(authResult);
137 | }
138 | }
139 | }
140 |
141 | ///
142 | /// Perform an HTTP GET request to a URL using an HTTP Authorization header
143 | ///
144 | /// The URL
145 | /// The token
146 | /// String containing the results of the GET operation
147 | public async Task GetHttpContentWithToken(string url, string token)
148 | {
149 | var httpClient = new HttpClient();
150 | HttpResponseMessage response;
151 | try
152 | {
153 | var request = new HttpRequestMessage(HttpMethod.Get, url);
154 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
155 | response = await httpClient.SendAsync(request);
156 | var content = await response.Content.ReadAsStringAsync();
157 | return content;
158 | }
159 | catch (Exception ex)
160 | {
161 | return ex.ToString();
162 | }
163 | }
164 |
165 | private async void SignOutButton_Click(object sender, RoutedEventArgs e)
166 | {
167 | // SingOut will remove tokens from the token cache from ALL accounts, irrespective of user flow
168 | IEnumerable accounts = await App.PublicClientApp.GetAccountsAsync();
169 | try
170 | {
171 | while (accounts.Any())
172 | {
173 | await App.PublicClientApp.RemoveAsync(accounts.FirstOrDefault());
174 | accounts = await App.PublicClientApp.GetAccountsAsync();
175 | }
176 |
177 | UpdateSignInState(false);
178 | }
179 | catch (MsalException ex)
180 | {
181 | ResultText.Text = $"Error signing-out user: {ex.Message}";
182 | }
183 | }
184 |
185 | private async void Window_Loaded(object sender, RoutedEventArgs e)
186 | {
187 | try
188 | {
189 | var app = App.PublicClientApp;
190 | var accounts = await app.GetAccountsAsync(App.PolicySignUpSignIn);
191 |
192 | AuthenticationResult authResult = await app.AcquireTokenSilent(App.ApiScopes, accounts.FirstOrDefault())
193 | .ExecuteAsync();
194 |
195 | DisplayUserInfo(authResult);
196 | UpdateSignInState(true);
197 | }
198 | catch (MsalUiRequiredException)
199 | {
200 | // Ignore, user will need to sign in interactively.
201 | ResultText.Text = "You need to sign-in first, and then Call API";
202 | }
203 | catch (Exception ex)
204 | {
205 | ResultText.Text = $"Error Acquiring Token Silently:{Environment.NewLine}{ex}";
206 | }
207 | }
208 |
209 | private void UpdateSignInState(bool signedIn)
210 | {
211 | if (signedIn)
212 | {
213 | CallApiButton.Visibility = Visibility.Visible;
214 | EditProfileButton.Visibility = Visibility.Visible;
215 | SignOutButton.Visibility = Visibility.Visible;
216 |
217 | SignInButton.Visibility = Visibility.Collapsed;
218 | }
219 | else
220 | {
221 | ResultText.Text = "";
222 | TokenInfoText.Text = "";
223 |
224 | CallApiButton.Visibility = Visibility.Collapsed;
225 | EditProfileButton.Visibility = Visibility.Collapsed;
226 | SignOutButton.Visibility = Visibility.Collapsed;
227 |
228 | SignInButton.Visibility = Visibility.Visible;
229 | }
230 | }
231 |
232 | private void DisplayUserInfo(AuthenticationResult authResult)
233 | {
234 | if (authResult != null)
235 | {
236 | JObject user = ParseIdToken(authResult.IdToken);
237 |
238 | TokenInfoText.Text = "";
239 | TokenInfoText.Text += $"Name: {user["name"]?.ToString()}" + Environment.NewLine;
240 | TokenInfoText.Text += $"User Identifier: {user["oid"]?.ToString()}" + Environment.NewLine;
241 | TokenInfoText.Text += $"Street Address: {user["streetAddress"]?.ToString()}" + Environment.NewLine;
242 | TokenInfoText.Text += $"City: {user["city"]?.ToString()}" + Environment.NewLine;
243 | TokenInfoText.Text += $"State: {user["state"]?.ToString()}" + Environment.NewLine;
244 | TokenInfoText.Text += $"Country: {user["country"]?.ToString()}" + Environment.NewLine;
245 | TokenInfoText.Text += $"Job Title: {user["jobTitle"]?.ToString()}" + Environment.NewLine;
246 |
247 | if (user["emails"] is JArray emails)
248 | {
249 | TokenInfoText.Text += $"Emails: {emails[0].ToString()}" + Environment.NewLine;
250 | }
251 | TokenInfoText.Text += $"Identity Provider: {user["iss"]?.ToString()}" + Environment.NewLine;
252 | }
253 | }
254 |
255 | JObject ParseIdToken(string idToken)
256 | {
257 | // Parse the idToken to get user info
258 | idToken = idToken.Split('.')[1];
259 | idToken = Base64UrlDecode(idToken);
260 | return JObject.Parse(idToken);
261 | }
262 |
263 | private string Base64UrlDecode(string s)
264 | {
265 | s = s.Replace('-', '+').Replace('_', '/');
266 | s = s.PadRight(s.Length + (4 - s.Length % 4) % 4, '=');
267 | var byteArray = Convert.FromBase64String(s);
268 | var decoded = Encoding.UTF8.GetString(byteArray, 0, byteArray.Count());
269 | return decoded;
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------