();
65 |
66 | var request = new PatTokenCreateRequest
67 | {
68 | DisplayName = "Generated by sample code",
69 | Scope = "vso.work vso.graph"
70 | };
71 |
72 | var patTokenResult = await client.CreatePatAsync(request);
73 |
74 | if (patTokenResult.PatTokenError == SessionTokenError.None)
75 | {
76 | Console.WriteLine($"PAT Secret: {patTokenResult.PatToken.Token}");
77 | }
78 | else
79 | {
80 | Console.WriteLine($"Error: {patTokenResult.PatTokenError}");
81 | }
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/NonInteractivePatGenerationSample/README.md:
--------------------------------------------------------------------------------
1 | # Non-interactive PAT generation sample
2 |
3 | This sample shows how to generate a Personal Access Token (PAT) using the [Client Libraries](https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/dotnet-client-libraries?view=azure-devops) and the [PAT Lifecycle Management API](https://learn.microsoft.com/en-us/rest/api/azure/devops/tokens). Requests to this API need to be authorized with an Azure Active Directory (AAD) access token.
4 |
5 | This sample uses the `PublicClientApplicationBuilder` from **Microsoft Authentication Library (MSAL)** rather than relying on the interactive pop-up dialog to get an AAD access token which is then used as a credential to authenticate requests to Azure DevOps. This is meant to be used in scenarios where you need to generate a PAT associated with an account that does not have interactive login rights.
6 |
7 | ## How to run this sample
8 |
9 | **Prerequisites**
10 |
11 | - [.NET Framework 4.7.2 SDK](https://dotnet.microsoft.com/en-us/download/dotnet-framework)
12 | - [An Application in your Azure Active Directory (Azure AD) tenant](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)
13 | - _(optional)_ [Visual Studio / Visual Studio Code](https://visualstudio.microsoft.com/downloads/)
14 |
15 |
16 | ### Step 1: Clone or download this repository
17 |
18 | From a shell or command line:
19 | ```
20 | git clone https://github.com/microsoft/azure-devops-auth-samples.git
21 | ```
22 |
23 | ### Step 2: Configure the sample to use your Azure AD application
24 |
25 | Update the configuration file `App.config` with the information about your AAD application, AAD credentials, and Azure DevOps organization.
26 |
27 | ### Step 3: Run the sample
28 |
29 | **From Visual Studio:**
30 | 1. Open the solution file `NonInteractivePatGenerationSample.sln`.
31 | 2. Build and run the project.
32 |
33 | -- OR --
34 |
35 | **From the command line:**
36 | ```cmd
37 | cd NonInteractivePatGenerationSample/NonInteractivePatGenerationSample
38 | dotnet run
39 | ```
40 |
--------------------------------------------------------------------------------
/OAuthWebSample/.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 | *.sln.docstates
8 | *.sln.ide/
9 | OAuthWebSample/Properties/PublishProfiles/
10 |
11 | # Build results
12 |
13 | [Dd]ebug/
14 | [Rr]elease/
15 | x64/
16 | build/
17 | [Bb]in/
18 | [Oo]bj/
19 |
20 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
21 | !packages/*/build/
22 |
23 | # MSTest test Results
24 | [Tt]est[Rr]esult*/
25 | [Bb]uild[Ll]og.*
26 |
27 | *_i.c
28 | *_p.c
29 | *.ilk
30 | *.meta
31 | *.obj
32 | *.pch
33 | *.pdb
34 | *.pgc
35 | *.pgd
36 | *.rsp
37 | *.sbr
38 | *.tlb
39 | *.tli
40 | *.tlh
41 | *.tmp
42 | *.tmp_proj
43 | *.log
44 | *.vspscc
45 | *.vssscc
46 | .builds
47 | *.pidb
48 | *.log
49 | *.scc
50 |
51 | # Visual C++ cache files
52 | ipch/
53 | *.aps
54 | *.ncb
55 | *.opensdf
56 | *.sdf
57 | *.cachefile
58 |
59 | # Visual Studio profiler
60 | *.psess
61 | *.vsp
62 | *.vspx
63 |
64 | # Guidance Automation Toolkit
65 | *.gpState
66 |
67 | # ReSharper is a .NET coding add-in
68 | _ReSharper*/
69 | *.[Rr]e[Ss]harper
70 |
71 | # TeamCity is a build add-in
72 | _TeamCity*
73 |
74 | # DotCover is a Code Coverage Tool
75 | *.dotCover
76 |
77 | # NCrunch
78 | *.ncrunch*
79 | .*crunch*.local.xml
80 |
81 | # Installshield output folder
82 | [Ee]xpress/
83 |
84 | # DocProject is a documentation generator add-in
85 | DocProject/buildhelp/
86 | DocProject/Help/*.HxT
87 | DocProject/Help/*.HxC
88 | DocProject/Help/*.hhc
89 | DocProject/Help/*.hhk
90 | DocProject/Help/*.hhp
91 | DocProject/Help/Html2
92 | DocProject/Help/html
93 |
94 | # Click-Once directory
95 | publish/
96 |
97 | # Publish Web Output
98 | *.Publish.xml
99 |
100 | # NuGet Packages Directory
101 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
102 | packages/
103 |
104 | # Windows Azure Build Output
105 | csx
106 | *.build.csdef
107 |
108 | # Windows Store app package directory
109 | AppPackages/
110 |
111 | # Others
112 | sql/
113 | *.Cache
114 | ClientBin/
115 | [Ss]tyle[Cc]op.*
116 | ~$*
117 | *~
118 | *.dbmdl
119 | *.[Pp]ublish.xml
120 | *.pfx
121 | *.publishsettings
122 |
123 | # RIA/Silverlight projects
124 | Generated_Code/
125 |
126 | # Backup & report files from converting an old project file to a newer
127 | # Visual Studio version. Backup files are not needed, because we have git ;-)
128 | _UpgradeReport_Files/
129 | Backup*/
130 | UpgradeLog*.XML
131 | UpgradeLog*.htm
132 |
133 | # SQL Server files
134 | App_Data/*.mdf
135 | App_Data/*.ldf
136 |
137 |
138 | #LightSwitch generated files
139 | GeneratedArtifacts/
140 | _Pvt_Extensions/
141 | ModelManifest.xml
142 |
143 | # =========================
144 | # Windows detritus
145 | # =========================
146 |
147 | # Windows image file caches
148 | Thumbs.db
149 | ehthumbs.db
150 |
151 | # Folder config file
152 | Desktop.ini
153 |
154 | # Recycle Bin used on file shares
155 | $RECYCLE.BIN/
156 |
157 | # Mac desktop service store files
158 | .DS_Store
159 |
160 |
161 | # =====
162 | # Other
163 | # =====
164 | storage*
165 | db.lock
166 | *.pubxml
167 |
168 | .vs/
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.4.33213.308
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OAuthWebSample", "OAuthWebSample\OAuthWebSample.csproj", "{6D848F3D-473F-45FD-8315-8FAB2F88815A}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishScripts", "PublishScripts", "{4D26F01F-9D2A-45AC-B807-53E66EC65B72}"
9 | ProjectSection(SolutionItems) = preProject
10 | PublishScripts\AzureWebAppPublishModule.psm1 = PublishScripts\AzureWebAppPublishModule.psm1
11 | PublishScripts\Publish-WebApplication.ps1 = PublishScripts\Publish-WebApplication.ps1
12 | EndProjectSection
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configurations", "Configurations", "{F0D344B9-A8BE-4147-909F-3214E45204A1}"
15 | ProjectSection(SolutionItems) = preProject
16 | PublishScripts\Configurations\OAuthSample-WAWS-dev.json = PublishScripts\Configurations\OAuthSample-WAWS-dev.json
17 | EndProjectSection
18 | EndProject
19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9099F192-D034-4AB2-B628-EDB4B4C2BB81}"
20 | ProjectSection(SolutionItems) = preProject
21 | README.md = README.md
22 | EndProjectSection
23 | EndProject
24 | Global
25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
26 | Debug|Any CPU = Debug|Any CPU
27 | Release|Any CPU = Release|Any CPU
28 | EndGlobalSection
29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
30 | {6D848F3D-473F-45FD-8315-8FAB2F88815A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {6D848F3D-473F-45FD-8315-8FAB2F88815A}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {6D848F3D-473F-45FD-8315-8FAB2F88815A}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {6D848F3D-473F-45FD-8315-8FAB2F88815A}.Release|Any CPU.Build.0 = Release|Any CPU
34 | EndGlobalSection
35 | GlobalSection(SolutionProperties) = preSolution
36 | HideSolutionNode = FALSE
37 | EndGlobalSection
38 | GlobalSection(ExtensibilityGlobals) = postSolution
39 | SolutionGuid = {0348F5DD-66F1-4AA9-B291-FDEA2E19AC3A}
40 | EndGlobalSection
41 | EndGlobal
42 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/App_Start/BundleConfig.cs:
--------------------------------------------------------------------------------
1 | using System.Web;
2 | using System.Web.Optimization;
3 |
4 | namespace OAuthSample
5 | {
6 | public class BundleConfig
7 | {
8 | // For more information on bundling, visit https://go.microsoft.com/fwlink/?LinkId=301862
9 | public static void RegisterBundles(BundleCollection bundles)
10 | {
11 | bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
12 | "~/Scripts/jquery-{version}.js"));
13 |
14 | bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
15 | "~/Scripts/jquery.validate*"));
16 |
17 | // Use the development version of Modernizr to develop with and learn from. Then, when you're
18 | // ready for production, use the build tool at https://modernizr.com to pick only the tests you need.
19 | bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
20 | "~/Scripts/modernizr-*"));
21 |
22 | bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
23 | "~/Scripts/bootstrap.js"));
24 |
25 | bundles.Add(new StyleBundle("~/Content/css").Include(
26 | "~/Content/bootstrap.css",
27 | "~/Content/site.css"));
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/App_Start/FilterConfig.cs:
--------------------------------------------------------------------------------
1 | using System.Web;
2 | using System.Web.Mvc;
3 |
4 | namespace OAuthSample
5 | {
6 | public class FilterConfig
7 | {
8 | public static void RegisterGlobalFilters(GlobalFilterCollection filters)
9 | {
10 | filters.Add(new HandleErrorAttribute());
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/App_Start/RouteConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 | using System.Web.Mvc;
6 | using System.Web.Routing;
7 |
8 | namespace OAuthSample
9 | {
10 | public class RouteConfig
11 | {
12 | public static void RegisterRoutes(RouteCollection routes)
13 | {
14 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
15 |
16 | routes.MapRoute(
17 | name: "Default",
18 | url: "{controller}/{action}/{id}",
19 | defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
20 | );
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Content/Site.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 50px;
3 | padding-bottom: 20px;
4 |
5 | line-height: 1.5em;
6 | }
7 |
8 | body, h1, h2, h3, h4, h5, h6 {
9 | font-family: "Segoe UI", Verdana, sans-serif;
10 | }
11 |
12 | /* Set padding to keep content from hitting the edges */
13 | .body-content {
14 | padding-left: 15px;
15 | padding-right: 15px;
16 | }
17 |
18 | /* Set width on the form input elements since they're 100% wide by default */
19 | input,
20 | select,
21 | textarea {
22 | max-width: 280px;
23 | }
24 |
25 | /* styles for validation helpers */
26 | .field-validation-error {
27 | color: #b94a48;
28 | }
29 |
30 | .field-validation-valid {
31 | display: none;
32 | }
33 |
34 | input.input-validation-error {
35 | border: 1px solid #b94a48;
36 | }
37 |
38 | input[type="checkbox"].input-validation-error {
39 | border: 0 none;
40 | }
41 |
42 | .validation-summary-errors {
43 | color: #b94a48;
44 | }
45 |
46 | .validation-summary-valid {
47 | display: none;
48 | }
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Content/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/OAuthWebSample/OAuthWebSample/Content/logo.png
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Controllers/HomeController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 | using System.Web.Mvc;
6 |
7 | namespace OAuthSample.Controllers
8 | {
9 | public class HomeController : Controller
10 | {
11 | public ActionResult Index()
12 | {
13 | ViewBag.ClientAppId = System.Configuration.ConfigurationManager.AppSettings["ClientAppId"];
14 | ViewBag.CallbackUrl = System.Configuration.ConfigurationManager.AppSettings["CallbackUrl"];
15 | ViewBag.Scope = System.Configuration.ConfigurationManager.AppSettings["Scope"];
16 |
17 | return View();
18 | }
19 |
20 | public ActionResult About()
21 | {
22 | ViewBag.Message = "Your application description page.";
23 |
24 | return View();
25 | }
26 |
27 | public ActionResult Contact()
28 | {
29 | ViewBag.Message = "Your contact page.";
30 |
31 | return View();
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Global.asax:
--------------------------------------------------------------------------------
1 | <%@ Application Codebehind="Global.asax.cs" Inherits="OAuthSample.MvcApplication" Language="C#" %>
2 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Global.asax.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 | using System.Web.Mvc;
6 | using System.Web.Optimization;
7 | using System.Web.Routing;
8 |
9 | namespace OAuthSample
10 | {
11 | public class MvcApplication : System.Web.HttpApplication
12 | {
13 | protected void Application_Start()
14 | {
15 | AreaRegistration.RegisterAllAreas();
16 | FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
17 | RouteConfig.RegisterRoutes(RouteTable.Routes);
18 | BundleConfig.RegisterBundles(BundleTable.Bundles);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Models/TokenModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace OAuthSample.Models
5 | {
6 | [DataContract]
7 | public class TokenModel
8 | {
9 | [DataMember(Name = "access_token")]
10 | public String AccessToken { get; set; }
11 |
12 | [DataMember(Name = "token_type")]
13 | public String TokenType { get; set; }
14 |
15 | [DataMember(Name = "refresh_token")]
16 | public String RefreshToken { get; set; }
17 |
18 | [DataMember(Name = "expires_in")]
19 | public int ExpiresIn { get; set; }
20 |
21 | public bool IsPending { get; set; }
22 | }
23 | }
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("OAuthSample")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("Microsoft Corp.")]
12 | [assembly: AssemblyProduct("OAuthSample")]
13 | [assembly: AssemblyCopyright("Copyright © Microsoft Corp. 2014")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("4eba735e-4ab8-4817-b79c-03e850605d48")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Revision and Build Numbers
33 | // by using the '*' as shown below:
34 | [assembly: AssemblyVersion("1.0.0.0")]
35 | [assembly: AssemblyFileVersion("1.0.0.0")]
36 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/Home/About.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewBag.Title = "About";
3 | }
4 | @ViewBag.Title.
5 | @ViewBag.Message
6 |
7 | Use this area to provide additional information.
8 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/Home/Contact.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewBag.Title = "Contact";
3 | }
4 | @ViewBag.Title.
5 | @ViewBag.Message
6 |
7 |
8 | One Microsoft Way
9 | Redmond, WA 98052-6399
10 | P:
11 | 425.555.0100
12 |
13 |
14 |
15 | Support: Support@example.com
16 | Marketing: Marketing@example.com
17 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/Home/Index.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewBag.Title = "Azure DevOps OAuth Client Sample";
3 |
4 | var missingMsg = "Not set - update web.config";
5 | var clientAppIdVal = !String.IsNullOrEmpty(ViewBag.ClientAppId) ? ViewBag.ClientAppId : missingMsg;
6 | var scopeVal = !String.IsNullOrEmpty(ViewBag.Scope) ? ViewBag.Scope : missingMsg;
7 | var callbackUrlVal = !String.IsNullOrEmpty(ViewBag.CallbackUrl) ? ViewBag.CallbackUrl : missingMsg;
8 | }
9 |
10 |
11 |
Azure DevOps OAuth Client Sample
12 |
This app shows how to authorize a user to authorize an app and then to request an access token to access Azure DevOps on their behalf.
13 |
Authorize »
14 |
15 |
16 |
17 |
18 |
Setup
19 |
20 | Before you start, make sure to:
21 |
22 | Register a client app with Azure DevOps
23 | Update the web.config of this web app and set the App ID, Scope, App Secret, and Callback URL set in the registered app. The callback URL should be https://site /oauth/callback
24 |
25 | App ID: @clientAppIdVal
26 | Scope: @scopeVal
27 | Callback URL: @callbackUrlVal
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Learn about auth in Azure DevOps
35 |
Azure DevOps supports authorization via OAuth 2.0. Learn more about how you can develop apps that securely access your user's Azure DevOps projects and perform tasks on their behalf.
36 |
Learn more »
37 |
38 |
39 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/OAuth/TokenView.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewBag.Title = "Details";
3 | }
4 |
5 |
18 |
19 | Token details
20 |
21 | @if (!String.IsNullOrEmpty(ViewBag.Error))
22 | {
23 | @ViewBag.Error
24 | }
25 | else
26 | {
27 |
48 |
49 |
63 | }
64 |
65 | View profile
66 | Return to start
67 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/Shared/Error.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = null;
3 | }
4 |
5 |
6 |
7 |
8 |
9 | Error
10 |
11 |
12 |
13 | Error.
14 | An error occurred while processing your request.
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @ViewBag.Title
7 | @Styles.Render("~/Content/css")
8 | @Scripts.Render("~/bundles/modernizr")
9 |
10 |
11 |
23 |
25 | @RenderBody()
26 |
27 |
28 |
29 | @Scripts.Render("~/bundles/jquery")
30 | @Scripts.Render("~/bundles/bootstrap")
31 | @RenderSection("scripts", required: false)
32 |
33 |
34 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/Web.config:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Views/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "~/Views/Shared/_Layout.cshtml";
3 | }
4 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Web.Debug.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Web.Release.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
19 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/Web.config:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/OAuthWebSample/OAuthWebSample/favicon.ico
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/OAuthWebSample/OAuthWebSample/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/OAuthWebSample/OAuthWebSample/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/OAuthWebSample/PublishScripts/Configurations/OAuthSample-WAWS-dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "environmentSettings": {
3 | "webSite": {
4 | "name": "OAuthSample",
5 | "location": "East US"
6 | },
7 | "databases": [
8 | {
9 | "connectionStringName": "",
10 | "databaseName": "",
11 | "serverName": "",
12 | "user": "",
13 | "password": "",
14 | "edition": "",
15 | "size": "",
16 | "collation": ""
17 | }
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/OAuthWebSample/README.md:
--------------------------------------------------------------------------------
1 | # ASP.NET web app (Azure DevOps OAuth sample)
2 |
3 | This sample shows how to prompt a user to authorize a cloud service that can call APIs on Azure DevOps on behalf of the user.
4 |
5 | To learn more about OAuth in Azure DevOps, see [Authorize access to Azure DevOps with OAuth 2.0](https://docs.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=vsts)
6 |
7 |
8 | ## How to setup
9 |
10 | > These instructions assume you will be deploying this sample app to an Azure web app. To learn more and to get started, visit [Get started with Azure Web Apps and ASP.NET](https://docs.microsoft.com/azure/app-service/app-service-web-get-started-dotnet-framework).
11 |
12 | 1. Register an OAuth client app in Azure DevOps (https://app.vsaex.visualstudio.com/app/register)
13 | * The callback URL should be https://yoursite.azurewebsites.net/oauth/callback, where `yoursite` is the name of your Azure web app
14 |
15 | 2. Clone this repository and open the solution `OAuthWebSample\OAuthWebSample.sln` in Visual Studio 2015 or later
16 |
17 | 3. Update the following settings in web.config to match the values in the app you just registered:
18 | * `ClientAppID`
19 | * `ClientAppSecret` (use the "Client Secret" shown on the Azure DevOps Application Settings page, not the App Secret)
20 | * `Scope` (space separated)
21 | * `CallbackUrl`
22 |
23 | 4. Build the solution (this will trigger a NuGet package restore, which will pull in all dependencies of the project)
24 |
25 | 5. Publish the app to Azure
26 |
27 | ### Run the sample
28 |
29 | 1. Navigate to your app (https://yoursite.azurewebsites.net)
30 |
31 | 2. Confirm your App ID, scope, and callback URL are displayed properly
32 | 
33 |
34 | 3. Click **Authorize**
35 |
36 | 4. Sign in to Azure DevOps (if prompted)
37 |
38 | 5. Review and accept the authorization request
39 |
40 | If everything is setup properly, Azure DevOps will issue an access token and refresh token and both values will be displayed. **You should keep these values secret**. Also a new authorization will appear in [your profile page](https://app.vssps.visualstudio.com/Profile/View).
41 |
42 |
43 | With the access token you can invoke [Azure DevOps REST APIs](https://docs.microsoft.com/en-us/rest/api/vsts/?view=vsts-rest-4.1) by providing the access token in the Authorization header.
44 |
45 | ```
46 | Authorization: Bearer {access token}
47 | ```
48 |
--------------------------------------------------------------------------------
/OAuthWebSample/appstart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/OAuthWebSample/appstart.png
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/.gitignore:
--------------------------------------------------------------------------------
1 | flask_session
2 | __pycache__
3 | .vscode
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/AppCreationScripts/Apps.json:
--------------------------------------------------------------------------------
1 | {
2 | "Sample": {
3 | "Title": "Integrating Azure AD into a Python web application",
4 | "Level": 400,
5 | "Client": "Python, MSAL.Python"
6 | },
7 | "AppRegistrations": [
8 | {
9 | "x-ms-id": "PythonWebApp",
10 | "x-ms-name": "ms-identity-python-webapp",
11 | "x-ms-version": "2.0",
12 | "replyUrlsWithType": [
13 | {
14 | "url": "http://localhost:5000/getAToken",
15 | "type": "Web"
16 | }
17 | ],
18 | "passwordCredentials": [
19 | {
20 | "value": "{auto}"
21 | }
22 | ],
23 | "x-ms-passwordCredentials": "Auto",
24 | "oauth2AllowImplicitFlow": false,
25 | "oauth2AllowIdTokenImplicitFlow": false,
26 | "requiredResourceAccess": [
27 | {
28 | "x-ms-resourceAppName": "Microsoft Graph",
29 | "resourceAppId": "00000003-0000-0000-c000-000000000000",
30 | "resourceAccess": [
31 | {
32 | "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
33 | "type": "Scope",
34 | "x-ms-name": "user.read"
35 | },
36 | {
37 | "id": "b340eb25-3456-403f-be2f-af7a0d370277",
38 | "type": "Scope",
39 | "x-ms-name": "User.ReadBasic.All"
40 | }
41 | ]
42 | }
43 | ],
44 | "codeConfigurations": [
45 | {
46 | "settingFile": "/app_config.py",
47 | "replaceTokens": {
48 | "appId": "Enter_the_Application_Id_here",
49 | "tenantId": "common",
50 | "clientSecret": "Enter_the_Client_Secret_Here",
51 | "authorityEndpointHost": "https://login.microsoftonline.com/",
52 | "msgraphEndpointHost": "https://graph.microsoft.com/"
53 | }
54 | }
55 | ]
56 | }
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/AppCreationScripts/Cleanup.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | param(
3 | [PSCredential] $Credential,
4 | [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')]
5 | [string] $tenantId
6 | )
7 |
8 | if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) {
9 | Install-Module "AzureAD" -Scope CurrentUser
10 | }
11 | Import-Module AzureAD
12 | $ErrorActionPreference = "Stop"
13 |
14 | Function Cleanup
15 | {
16 | <#
17 | .Description
18 | This function removes the Azure AD applications for the sample. These applications were created by the Configure.ps1 script
19 | #>
20 |
21 | # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant
22 | # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD.
23 |
24 | # Login to Azure PowerShell (interactive if credentials are not already provided:
25 | # you'll need to sign-in with creds enabling your to create apps in the tenant)
26 | if (!$Credential -and $TenantId)
27 | {
28 | $creds = Connect-AzureAD -TenantId $tenantId
29 | }
30 | else
31 | {
32 | if (!$TenantId)
33 | {
34 | $creds = Connect-AzureAD -Credential $Credential
35 | }
36 | else
37 | {
38 | $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential
39 | }
40 | }
41 |
42 | if (!$tenantId)
43 | {
44 | $tenantId = $creds.Tenant.Id
45 | }
46 | $tenant = Get-AzureADTenantDetail
47 | $tenantName = ($tenant.VerifiedDomains | Where-Object { $_._Default -eq $True }).Name
48 |
49 | # Removes the applications
50 | Write-Host "Cleaning-up applications from tenant '$tenantName'"
51 |
52 | Write-Host "Removing 'pythonwebapp' (python-webapp) if needed"
53 | Get-AzureADApplication -Filter "DisplayName eq 'python-webapp'" | ForEach-Object {Remove-AzureADApplication -ObjectId $_.ObjectId }
54 | $apps = Get-AzureADApplication -Filter "DisplayName eq 'python-webapp'"
55 | if ($apps)
56 | {
57 | Remove-AzureADApplication -ObjectId $apps.ObjectId
58 | }
59 |
60 | foreach ($app in $apps)
61 | {
62 | Remove-AzureADApplication -ObjectId $app.ObjectId
63 | Write-Host "Removed python-webapp.."
64 | }
65 | # also remove service principals of this app
66 | Get-AzureADServicePrincipal -filter "DisplayName eq 'python-webapp'" | ForEach-Object {Remove-AzureADServicePrincipal -ObjectId $_.Id -Confirm:$false}
67 |
68 | }
69 |
70 | Cleanup -Credential $Credential -tenantId $TenantId
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/AppCreationScripts/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "Sample": {
3 | "RepositoryUrl": "https://github.com/Azure-Samples/ms-identity-python-webapp",
4 | "Title": "Integrating Microsoft Identity Platform with a Python web application",
5 | "Level": 300,
6 | "Client": "Python Web Application",
7 | "Service": "Microsoft Graph",
8 | "Endpoint": "Microsoft identity platform (formerly Azure AD v2.0)"
9 | },
10 |
11 | /*
12 | This section describes the Azure AD Applications to configure, and their dependencies
13 | */
14 | "AADApps": [
15 | {
16 | "Id": "pythonwebapp",
17 | "Name": "python-webapp",
18 | "Kind": "WebApp", /* SinglePageApplication, WebApp, Mobile, UWP, Desktop, Daemon, WebApi, Browserless */
19 | "Audience": "AzureADandPersonalMicrosoftAccount", /* AzureADMyOrg, AzureADMultipleOrgs, AzureADandPersonalMicrosoftAccount, PersonalMicrosoftAccount */
20 | "PasswordCredentials": "Auto",
21 | "RequiredResourcesAccess": [
22 | {
23 | "Resource": "Microsoft Graph",
24 | "DelegatedPermissions": [
25 | "User.ReadBasic.All"
26 | ]
27 | }
28 | ],
29 | "ReplyUrls": "http://localhost:5000/getAToken"
30 | }
31 | ],
32 |
33 | /*
34 | This section describes how to update the code in configuration files from the apps coordinates, once the apps
35 | are created in Azure AD.
36 | Each section describes a configuration file, for one of the apps, it's type (XML, JSon, plain text), its location
37 | with respect to the root of the sample, and the mappping (which string in the config file is mapped to which value
38 | */
39 | "CodeConfiguration": [
40 | {
41 | "App": "pythonwebapp",
42 | "SettingKind": "Replace",
43 | "SettingFile": "\\..\\app_config.py",
44 | "Mappings": [
45 | {
46 | "key": "Enter_the_Tenant_Name_Here",
47 | "value": "$tenantName"
48 | },
49 | {
50 | "key": "Enter_the_Client_Secret_Here",
51 | "value": ".AppKey"
52 | },
53 | {
54 | "key": "Enter_the_Application_Id_here",
55 | "value": ".AppId"
56 | }
57 | ]
58 | }
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/ReadmeFiles/topology.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/azure-devops-auth-samples/9097287405fde466a745dfb94c2dcaf0860a79e9/PersonalAccessTokenAPIAppSample/ReadmeFiles/topology.png
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/app_config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # To configure this application, fill in your application (client) ID, client secret,
4 | # AAD tenant ID, and Azure DevOps collection name in the placeholders below.
5 |
6 | CLIENT_ID = "Enter_the_Application_Id_here"
7 | # Application (client) ID of app registration
8 |
9 | CLIENT_SECRET = "Enter_the_Client_Secret_here"
10 | # In a production app, we recommend you use a more secure method of storing your secret,
11 | # like Azure Key Vault. Or, use an environment variable as described in Flask's documentation:
12 | # https://flask.palletsprojects.com/en/1.1.x/config/#configuring-from-environment-variables
13 | # CLIENT_SECRET = os.getenv("CLIENT_SECRET")
14 | # if not CLIENT_SECRET:
15 | # raise ValueError("Need to define CLIENT_SECRET environment variable")
16 |
17 | AUTHORITY = "https://login.microsoftonline.com/Enter_the_Tenant_ID_Here" # For multi-tenant app
18 | # AUTHORITY = "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here"
19 |
20 | REDIRECT_PATH = "/getAToken" # Used for forming an absolute URL to your redirect URI.
21 | # The absolute URL must match the redirect URI you set
22 | # in the app's registration in the Azure portal.
23 |
24 |
25 | ENDPOINT = 'https://vssps.dev.azure.com/Enter_the_Collection_Name_Here/_apis/Tokens/Pats?api-version=6.1-preview'
26 | # fill in the url to the user's ADO collection name here
27 |
28 | SCOPE = ["499b84ac-1321-427f-aa17-267ca6975798/.default"]
29 | # Means "All scopes for the Azure DevOps API resource"
30 |
31 | SESSION_TYPE = "filesystem"
32 | # Specifies the token cache should be stored in server-side session
33 |
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask>=1,<2
2 | werkzeug>=1,<2
3 | flask-session~=0.3.2
4 | requests>=2,<3
5 | msal>=0.6.1,<2
6 |
7 |
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/templates/auth_error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% if config.get("B2C_RESET_PASSWORD_AUTHORITY") and "AADB2C90118" in result.get("error_description") %}
7 |
8 |
9 | {% endif %}
10 |
11 |
12 | Login Failure
13 |
14 | {{ result.get("error") }}
15 | {{ result.get("error_description") }}
16 |
17 |
18 | Homepage
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/templates/display.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Back
8 | PAT Lifecycle API Call Result
9 | {{ result |tojson(indent=4) }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/PersonalAccessTokenAPIAppSample/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Microsoft Identity Python Web App
8 |
9 | Sign In
10 |
11 | {% if config.get("B2C_RESET_PASSWORD_AUTHORITY") %}
12 | Reset Password
13 | {% endif %}
14 |
15 |
16 | Powered by MSAL Python {{ version }}
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Auth samples for Azure DevOps Services
2 |
3 | 
4 |
5 | Samples that show how to authenticate with Azure DevOps and Azure DevOps Server.
6 |
7 | Learn more about [integrating with Azure DevOps](https://docs.microsoft.com/en-us/azure/devops/extend/overview?view=vsts) and [specific authentication guidance](https://docs.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/authentication-guidance?view=vsts)
8 |
9 | ## Samples
10 |
11 | * [Managed client sample (using Azure Active Directory Library)](./ManagedClientConsoleAppSample/README.md)
12 | * [Device profile sample (.NET Core)](./DeviceProfileSample/README.md)
13 | * [ASP.NET Web app OAuth sample](./OAuthWebSample/README.md)
14 | * [Client library sample (using VSSConnection)](./ClientLibraryConsoleAppSample/README.md)
15 | * [Javascript web app sample (using Microsoft Authentication Library for JavaScript)](./JavascriptWebAppSample/README.md)
16 | * [Dual Support (Azure DevOps/TFS) Client Sample (using Azure Active Directory Library and Windows Authentication)](./DualSupportClientSample/README.md)
17 | * [Non-interactive PAT Generation Sample (using Azure Active Directory Library with a Username Password credential)](./NonInteractivePatGenerationSample/README.md)
18 | * [PAT lifecycle management API sample (using Microsoft Authentication Library with authentication code)](./PersonalAccessTokenAPIAppSample/README.md)
19 | * [Azure AD Service Principals and Managed Identities in Azure DevOps](/ServicePrincipalsSamples/)
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [many more](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [definition](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center at [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://technet.microsoft.com/en-us/security/dn606155).
12 |
13 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
14 |
15 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
16 |
17 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
18 | * Full paths of source file(s) related to the manifestation of the issue
19 | * The location of the affected source code (tag/branch/commit or direct URL)
20 | * Any special configuration required to reproduce the issue
21 | * Step-by-step instructions to reproduce the issue
22 | * Proof-of-concept or exploit code (if possible)
23 | * Impact of the issue, including how an attacker might exploit the issue
24 |
25 | This information will help us triage your report more quickly.
26 |
27 | ## Preferred Languages
28 |
29 | We prefer all communications to be in English.
30 |
31 | ## Policy
32 |
33 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
34 |
35 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/0-SimpleConsoleApp-AppRegistration/0-SimpleConsoleApp-AppRegistration.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/0-SimpleConsoleApp-AppRegistration/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography.X509Certificates;
2 | using Azure.Core;
3 | using Azure.Identity;
4 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
5 | using Microsoft.VisualStudio.Services.Client;
6 | using Microsoft.VisualStudio.Services.WebApi;
7 | using Microsoft.VisualStudio.Services.WebApi.Patch;
8 | using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
9 |
10 |
11 | /// PARAMETERS
12 | const string AdoBaseUrl = "https://dev.azure.com";
13 | const string AdoOrgName = "Your organization name";
14 |
15 | const string AadTenantId = "Your Azure AD tenant id";
16 | const string AadClientId = ""; // Client ID for your App Registration / Service Principal
17 | // Set one of either clientSecret or certificateThumbprint
18 | const string AadClientSecret = ""; // Client Secret for your App Registration / Service Principal
19 | const string AadCertificateThumbprint = ""; // Thumbprint for your client certificate
20 |
21 |
22 | /// CODE
23 | TokenCredential credential;
24 |
25 | if (!string.IsNullOrEmpty(AadClientSecret))
26 | {
27 | credential = new ClientSecretCredential(AadTenantId, AadClientId, AadClientSecret);
28 | }
29 | else
30 | {
31 | using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); // Replace with appropriate Store Name / Location if necessary
32 | store.Open(OpenFlags.ReadOnly);
33 | var certificate = store.Certificates.Cast().FirstOrDefault(cert => cert.Thumbprint == AadCertificateThumbprint);
34 |
35 | credential = new ClientCertificateCredential(AadTenantId, AadClientId, certificate);
36 | }
37 |
38 | // Whenever possible, credential instance should be reused for the lifetime of the process.
39 | // An internal token cache is used which reduces the number of outgoing calls to Azure AD to get tokens.
40 | // Call GetTokenAsync whenever you are making a request. Token caching and refresh logic is handled by the credential object.
41 | var vssAadCredentials = new VssAzureIdentityCredential(credential);
42 |
43 | var orgUrl = new Uri(new Uri(AdoBaseUrl), AdoOrgName);
44 | var connection = new VssConnection(orgUrl, vssAadCredentials);
45 |
46 |
47 | var client = connection.GetClient();
48 |
49 | Console.Write("Work Item Command? [get or create] ");
50 | var command = Console.ReadLine().Trim().ToLowerInvariant();
51 | Console.WriteLine();
52 | Console.Write("Azure DevOps Project Name? ");
53 | var project = Console.ReadLine().Trim();
54 | Console.WriteLine();
55 |
56 | if (command == "get")
57 | {
58 | Console.Write("Work Item ID? ");
59 | var workItemId = int.Parse(Console.ReadLine().Trim());
60 | Console.WriteLine();
61 | var workItem = await client.GetWorkItemAsync(project, workItemId);
62 | Console.WriteLine(workItem.Fields["System.Title"]);
63 | }
64 | else if (command == "create")
65 | {
66 | Console.Write("Work Item Title? ");
67 | var title = Console.ReadLine().Trim();
68 | Console.WriteLine();
69 | var patchDocument = new JsonPatchDocument
70 | {
71 | new JsonPatchOperation()
72 | {
73 | Operation = Operation.Add,
74 | Path = "/fields/System.Title",
75 | Value = title
76 | }
77 | };
78 |
79 | try
80 | {
81 |
82 | var result = await client.CreateWorkItemAsync(patchDocument, project, "bug");
83 | Console.WriteLine($"Work item created: Id = {result.Id}");
84 | }
85 | catch (Exception ex)
86 | {
87 | Console.WriteLine(ex.Message);
88 | return -1;
89 | }
90 | }
91 |
92 |
93 | return 0;
94 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/0-SimpleConsoleApp-AppRegistration/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "0-ConsoleApp-AppRegistration": {
4 | "commandName": "Project",
5 | "commandLineArgs": ""
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/0-SimpleConsoleApp-AppRegistration/README.md:
--------------------------------------------------------------------------------
1 | # Simple .NET Core console application using an Azure AD Application to create/get work items
2 |
3 | This sample shows how to get an Azure AD access token for an Application Service Principal (using a secret or a certificate) using [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet) and authenticate to Azure DevOps to create or get a work item.
4 |
5 | ## How to run this sample
6 |
7 | **Prerequisites**
8 |
9 | - [.NET Core SDK](https://dotnet.microsoft.com/en-us/download) - 6.0 or higher
10 | - [Azure DevOps .NET client libraries](https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/dotnet-client-libraries?view=azure-devops) - 19.219.0-preview or higher
11 | - [Visual Studio / Visual Studio Code](https://aka.ms/vsdownload)
12 |
13 | ### Step 1: Clone or download this repository
14 |
15 | From a shell or command line:
16 |
17 | ```ps
18 | git clone https://github.com/microsoft/azure-devops-auth-samples.git
19 | ```
20 |
21 | ### Step 2: Create an Azure AD application
22 |
23 | In the tenant to which your Azure DevOps organization is connected, [create an Azure AD Application](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
24 |
25 | ### Step 3: Add the Azure AD application to your Azure DevOps Organization
26 |
27 | Once the application is created, [add it to your Azure DevOps organization](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity#step-by-step-configuration).
28 |
29 | ### Step 4: Configure the sample to use your Azure AD application
30 |
31 | Update parameters in the file `Program.cs` with the information about your Azure AD application and Azure DevOps organization.
32 |
33 | ```cs
34 | /// PARAMETERS
35 | const string orgName = "YOUR ORG NAME";
36 |
37 | const string tenantId = "YOUR TENANT ID";
38 | const string clientId = ""; // Client ID for your App Registration / Service Principal
39 | // Set one of either clientSecret or certificateThumbprint
40 | const string clientSecret = ""; // Client Secret for your App Registration / Service Principal
41 | const string certificateThumbprint = ""; // Thumbprint for your client certificate
42 | ```
43 |
44 | ### Step 5: Run the sample
45 |
46 | From the console:
47 |
48 | ```cmd
49 | cd 0-SimpleConsoleApp-AppRegistration
50 | dotnet run
51 | ```
52 |
53 | # References
54 |
55 | - [Azure.Identity - ClientCertificateCredentials](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientcertificatecredential?view=azure-dotnet)
56 | - [Azure.Identity - ClientSecretCredentials](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientsecretcredential?view=azure-dotnet)
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/1-ConsoleApp-AppRegistration.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | ServicePrincipalsSamples
7 |
8 |
9 |
10 | TRACE
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Aad/AadAccessTokenHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Services.Client;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace ServicePrincipalsSamples.Aad
8 | {
9 | ///
10 | /// Adds an Azure AD access token for Azure DevOps as authentication mechanism for every request
11 | ///
12 | public class AadAccessTokenHandler : DelegatingHandler
13 | {
14 | private readonly AadClient _aadClient;
15 |
16 | public AadAccessTokenHandler(AadClient aadClient)
17 | {
18 | _aadClient = aadClient;
19 | }
20 |
21 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
22 | {
23 | var result = await _aadClient.GetAadAccessToken(VssAadSettings.DefaultScopes);
24 | request.Headers.Authorization = new AuthenticationHeaderValue(result.TokenType, result.AccessToken);
25 | return await base.SendAsync(request, cancellationToken);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Aad/AadClient.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Identity.Client;
2 | using Microsoft.Identity.Web;
3 | using ServicePrincipalsSamples.Settings;
4 | using System;
5 | using System.Threading.Tasks;
6 |
7 | namespace ServicePrincipalsSamples.Aad
8 | {
9 | ///
10 | /// Azure AD client for a App Registration Service Principal
11 | ///
12 | public class AadClient
13 | {
14 | private const string ClientSecretPlaceholderValue = "[Enter here a client secret for your application]";
15 |
16 | private IConfidentialClientApplication app;
17 |
18 | public AadClient(AppConfiguration config)
19 | {
20 | Initialize(config);
21 | }
22 |
23 | private void Initialize(AppConfiguration config)
24 | {
25 | if (IsAppUsingClientSecret(config))
26 | {
27 | app = ConfidentialClientApplicationBuilder.Create(config.Aad.ClientId)
28 | .WithClientSecret(config.Aad.ClientSecret)
29 | .WithAuthority(config.Aad.Authority)
30 | .Build();
31 | }
32 | else
33 | {
34 | ICertificateLoader certificateLoader = new DefaultCertificateLoader();
35 | certificateLoader.LoadIfNeeded(config.Aad.Certificate);
36 |
37 | app = ConfidentialClientApplicationBuilder.Create(config.Aad.ClientId)
38 | .WithCertificate(config.Aad.Certificate.Certificate)
39 | .WithAuthority(config.Aad.Authority)
40 | .Build();
41 | }
42 |
43 | app.AddInMemoryTokenCache();
44 | }
45 |
46 | private static bool IsAppUsingClientSecret(AppConfiguration config)
47 | {
48 | if (!string.IsNullOrWhiteSpace(config.Aad.ClientSecret) && config.Aad.ClientSecret != ClientSecretPlaceholderValue)
49 | {
50 | return true;
51 | }
52 | else if (config.Aad.Certificate != null)
53 | {
54 | return false;
55 | }
56 | else
57 | {
58 | throw new ArgumentException("You must choose between using secret or certificate. Please update appsettings.json file.");
59 | }
60 | }
61 |
62 | ///
63 | /// Returns an Azure AD access token (client credentials). It uses an in-memory cache and it also regenerates the access token if it is expired.
64 | ///
65 | /// Valid Azure AD access token
66 | public async Task GetAadAccessToken(string[] scopes)
67 | {
68 | // Client credentials flow uses the cache by default
69 | var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
70 | Console.ForegroundColor = ConsoleColor.Green;
71 | Console.WriteLine($"Token acquired for the Service Principal (source: '{result.AuthenticationResultMetadata.TokenSource}')\n");
72 | Console.ResetColor();
73 |
74 | return result;
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/AdoClientBase.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Services.Identity;
2 | using Microsoft.VisualStudio.Services.WebApi;
3 |
4 | namespace ServicePrincipalsSamples.AdoClient
5 | {
6 | ///
7 | /// Azure DevOps REST client using client libs
8 | ///
9 | /// Azure DevOps HTTP Client
10 | public abstract class AdoClientBase where T : VssHttpClientBase
11 | {
12 | protected readonly AdoConnection adoConnection;
13 | private T client;
14 |
15 | protected AdoClientBase(AdoConnection adoConnection)
16 | {
17 | this.adoConnection = adoConnection;
18 | }
19 |
20 | public Identity GetAuthorizedIdentity()
21 | {
22 | return adoConnection.VssConnection.AuthorizedIdentity;
23 | }
24 |
25 | protected T GetClient()
26 | {
27 | client ??= adoConnection.VssConnection.GetClient();
28 | return client;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/AdoConnection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Services.Client;
2 | using Microsoft.VisualStudio.Services.Common;
3 | using Microsoft.VisualStudio.Services.WebApi;
4 | using ServicePrincipalsSamples.Aad;
5 | using ServicePrincipalsSamples.Settings;
6 | using System;
7 | using System.Net.Http;
8 |
9 | namespace ServicePrincipalsSamples.AdoClient
10 | {
11 | ///
12 | /// Wraps the Azure DevOps connection with the supported authentication mechanisms on this application
13 | ///
14 | public class AdoConnection : IDisposable
15 | {
16 | ///
17 | /// IMPORTANT: The VssConnection instance should be a singleton in the application.
18 | ///
19 | public VssConnection VssConnection { get; private set; }
20 |
21 | private bool disposedValue;
22 |
23 | public AdoConnection(AppConfiguration config, AadClient aadClient)
24 | {
25 | VssConnection = CreateVssConnection(config, aadClient);
26 | }
27 |
28 | private static VssConnection CreateVssConnection(AppConfiguration config, AadClient aadClient)
29 | {
30 | VssCredentials credentials;
31 |
32 | if (config.Ado.AdoAuthenticationMode == AdoAuthenticationMode.AdoPat)
33 | {
34 | credentials = CreateVssConnectionWithPAT(config);
35 | }
36 | else if (config.Ado.AdoAuthenticationMode == AdoAuthenticationMode.AadServicePrincipal)
37 | {
38 | credentials = CreateVssConnectionWithAadAccessToken(aadClient);
39 | } else
40 | {
41 | throw new InvalidOperationException($"Unsupported authentication mode: {config.Ado.AdoAuthenticationMode}");
42 | }
43 |
44 | var settings = VssClientHttpRequestSettings.Default.Clone();
45 | // Custom UserAgent with format: " ")
46 | // E.g.: "VSServices/16.170.30907.1 (NetStandard; Microsoft Windows 10.0.22621) Identity.ServicePrincipalsSamples/1.0 (1-ConsoleApp-AppRegistration)"
47 | settings.UserAgent = AppConfiguration.AppUserAgent;
48 |
49 | var innerHandlers = new VssHttpMessageHandler(credentials, settings);
50 |
51 | var delegatingHandlers = new DelegatingHandler[] { new AdoRequestHandler() };
52 |
53 | return new VssConnection(config.Ado.OrganizationUrl, innerHandlers, delegatingHandlers);
54 | }
55 |
56 | ///
57 | /// Creates credentials with an Azure AD Service Prinicpal acces token as authentication mechanism.
58 | /// The token regeneration once it is expired is handled by the AadClient.
59 | ///
60 | /// Azure AD client
61 | ///
62 | private static VssCredentials CreateVssConnectionWithAadAccessToken(AadClient aadClient)
63 | {
64 | var vssAadToken = new VssAadToken((scopes) => aadClient.GetAadAccessToken(scopes).SyncResultConfigured());
65 | return new VssAadCredential(vssAadToken);
66 | }
67 |
68 | ///
69 | /// Creates credentials with an Azure DevOps PAT as authentication mechanism
70 | ///
71 | /// app configuration
72 | ///
73 | private static VssCredentials CreateVssConnectionWithPAT(AppConfiguration config)
74 | {
75 | return new VssBasicCredential(string.Empty, config.Ado.Pat);
76 | }
77 |
78 | public void Dispose()
79 | {
80 | Dispose(disposing: true);
81 | GC.SuppressFinalize(this);
82 | }
83 |
84 | protected virtual void Dispose(bool disposing)
85 | {
86 | if (!disposedValue)
87 | {
88 | if (disposing)
89 | {
90 | VssConnection.Dispose();
91 | }
92 |
93 | disposedValue = true;
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/AdoRequestHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace ServicePrincipalsSamples
7 | {
8 | public class AdoRequestHandler : DelegatingHandler
9 | {
10 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
11 | {
12 | Console.WriteLine($"Request: {request.Method} {request.RequestUri}\n");
13 | return await base.SendAsync(request, cancellationToken);
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/AdoRestClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using System.Collections.Generic;
4 | using System.Net.Http;
5 | using Microsoft.AspNetCore.WebUtilities;
6 | using System.Text.Json.Nodes;
7 | using ServicePrincipalsSamples.Settings;
8 |
9 | namespace ServicePrincipalsSamples.AdoClient
10 | {
11 | public interface IAdoRestClient
12 | {
13 | Task GetWorkItem(int workItemId);
14 |
15 | Task AddAadUserToGroup(string adoGroupSD, string userObjectId);
16 | }
17 |
18 | ///
19 | /// Azure DevOps simple REST client not using client libs
20 | ///
21 | public class AdoRestClient : IAdoRestClient
22 | {
23 | internal const string SpsUrlFragment = "vssps";
24 |
25 | private readonly AppConfiguration config;
26 | private readonly HttpClient httpClient;
27 |
28 | public AdoRestClient(AppConfiguration config, HttpClient httpClient)
29 | {
30 | this.config = config;
31 | this.httpClient = httpClient;
32 | }
33 |
34 | public async Task GetWorkItem(int workItemId)
35 | {
36 | var path = $"_apis/wit/workItems/{workItemId}";
37 | var url = CreateBaseAdoOrgUrl(path, version: "7.1-preview.3");
38 |
39 | var responseString = await httpClient.GetStringAsync(url);
40 |
41 | return JsonNode.Parse(responseString);
42 | }
43 |
44 | public async Task AddAadUserToGroup(string adoGroupSD, string userObjectId)
45 | {
46 | var path = "_apis/Graph/users";
47 | var queryParams = new Dictionary
48 | {
49 | { "groupDescriptors", adoGroupSD }
50 | };
51 | var url = CreateAdoOrgUrlForService(SpsUrlFragment, path, queryParams);
52 |
53 | var user = new
54 | {
55 | originId = userObjectId
56 | };
57 |
58 | var response = await httpClient.PostAsJsonAsync(url, user);
59 | var responseString = await response.Content.ReadAsStringAsync();
60 |
61 | return JsonNode.Parse(responseString);
62 | }
63 |
64 | #region Private methods
65 |
66 | private string CreateBaseAdoOrgUrl(string path, Dictionary queryParams = null, string version = null)
67 | {
68 | return CreateAdoOrgUrl(config.Ado.OrganizationUrl, path, queryParams, version);
69 | }
70 |
71 | private string CreateAdoOrgUrlForService(string serviceUrlFragment, string path, Dictionary queryParams = null, string version = null)
72 | {
73 | return CreateAdoOrgUrl(GetOrgUrlWithFragment(serviceUrlFragment), path, queryParams, version);
74 | }
75 |
76 | private string CreateAdoOrgUrl(Uri baseUrl, string path, Dictionary queryParams, string version = null)
77 | {
78 | var uriBuilder = new UriBuilder(baseUrl)
79 | {
80 | Path = $"{config.Ado.Organization}/{path}"
81 | };
82 |
83 | queryParams ??= new Dictionary();
84 | queryParams.Add("api-version", version ?? AdoConfiguration.DefaultApiVersion);
85 |
86 | return QueryHelpers.AddQueryString(uriBuilder.ToString(), queryParams);
87 | }
88 |
89 | ///
90 | /// Returns the URL with the service (if specified). Example: https://vssps.dev.azure.com/{AdoOrgName}
91 | ///
92 | /// If not provided, it default to the ADO base URL
93 | public Uri GetOrgUrlWithFragment(string serviceUrlFragment)
94 | {
95 | var baseOrgUrl = config.Ado.OrganizationUrl;
96 | var uriBuilder = new UriBuilder(baseOrgUrl);
97 | uriBuilder.Host = $"{serviceUrlFragment}.{uriBuilder.Host}";
98 | return uriBuilder.Uri;
99 | }
100 |
101 | #endregion
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/GraphClient.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Services.Common;
2 | using Microsoft.VisualStudio.Services.Graph.Client;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace ServicePrincipalsSamples.AdoClient
8 | {
9 | public class GraphClient : AdoClientBase
10 | {
11 | public GraphClient(AdoConnection adoConnection) : base(adoConnection) { }
12 |
13 | public async Task AddAadUserToGroup(string adoGroupSD, string userObjectId)
14 | {
15 | var groupDescriptors = new[] { SubjectDescriptor.FromString(adoGroupSD) };
16 |
17 | var userContext = new GraphUserOriginIdCreationContext()
18 | {
19 | OriginId = userObjectId,
20 | };
21 |
22 | return await GetClient().CreateUserAsync(userContext, groupDescriptors);
23 | }
24 |
25 | ///
26 | /// Returns Azure AD users in the organization
27 | ///
28 | /// Number of pages to return (0 means all pages)
29 | /// Azure AD users in the organization
30 | public async Task> ListAadUsers(int pages = 0)
31 | {
32 | int pageCounter = 0;
33 | string continuationToken = null;
34 | IList users = new List();
35 |
36 | do
37 | {
38 | var page = await GetClient().ListUsersAsync(subjectTypes: new[] { "aad" }, continuationToken);
39 | users.AddRange(page.GraphUsers);
40 | continuationToken = page.ContinuationToken?.First();
41 | pageCounter++;
42 | } while ((pages == 0 || pageCounter < pages) && continuationToken != null);
43 |
44 | return users;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/MemberEntitlementsClient.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.VisualStudio.Services.MemberEntitlementManagement.WebApi;
3 | using System;
4 | using System.Collections.Generic;
5 | using Microsoft.VisualStudio.Services.Common;
6 |
7 | namespace ServicePrincipalsSamples.AdoClient
8 | {
9 | public class MemberEntitlementsClient : AdoClientBase
10 | {
11 | public MemberEntitlementsClient(AdoConnection adoConnection) : base(adoConnection) { }
12 |
13 | ///
14 | /// Returns users in the organization
15 | ///
16 | /// Number of pages to return (0 means all pages)
17 | /// users in the organization
18 | public async Task> SearchUserEntitlements(int pages = 0)
19 | {
20 | int pageCounter = 0;
21 | string continuationToken = null;
22 | IList users = new List();
23 |
24 | do
25 | {
26 | var page = await GetClient().SearchUserEntitlementsAsync(continuationToken, orderBy: "name");
27 | users.AddRange(page.Members);
28 | continuationToken = page.ContinuationToken;
29 | pageCounter++;
30 | } while ((pages == 0 || pageCounter < pages) && continuationToken != null);
31 |
32 | return users;
33 | }
34 |
35 | public async Task GetServicePrincipalEntitlement(Guid servicePrinicipalId)
36 | {
37 | return await GetClient().GetServicePrincipalEntitlementAsync(servicePrinicipalId);
38 | }
39 |
40 | public async Task GetServicePrincipalEntitlementMe()
41 | {
42 | var servicePrincipalId = adoConnection.VssConnection.AuthorizedIdentity.Id;
43 | return await GetServicePrincipalEntitlement(servicePrincipalId);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/ProjectsClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using System.Collections.Generic;
4 | using Microsoft.TeamFoundation.Core.WebApi;
5 |
6 | namespace ServicePrincipalsSamples.AdoClient
7 | {
8 | public class ProjectsClient : AdoClientBase
9 | {
10 | public ProjectsClient(AdoConnection adoConnection) : base(adoConnection) { }
11 |
12 | public async Task> ListProjects()
13 | {
14 | var projects = await GetClient().GetProjects();
15 |
16 | return projects;
17 | }
18 |
19 | public async Task CreateProject(string projectName)
20 | {
21 | var versionControlDictionary = new Dictionary
22 | {
23 | { "sourceControlType", "Git" }
24 | };
25 |
26 | var processTemplateDictionary = new Dictionary
27 | {
28 | { "templateTypeId", "6b724908-ef14-45cf-84f8-768b5384da45" }
29 | };
30 |
31 | var teamProject = new TeamProject
32 | {
33 | Name = projectName,
34 | Description = "This project was created from the sample console application.",
35 | Capabilities = new Dictionary>
36 | {
37 | { "versioncontrol", versionControlDictionary },
38 | { "processTemplate", processTemplateDictionary }
39 | }
40 | };
41 |
42 | await GetClient().QueueCreateProject(teamProject);
43 |
44 | Console.WriteLine($"Project '{projectName}' queued for creation...");
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AdoClient/WorkItemsClient.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
2 | using Microsoft.VisualStudio.Services.WebApi.Patch;
3 | using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
4 | using System.Threading.Tasks;
5 | using WorkItem = Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.WorkItem;
6 |
7 | namespace ServicePrincipalsSamples.AdoClient
8 | {
9 | public class WorkItemsClient : AdoClientBase
10 | {
11 | public WorkItemsClient(AdoConnection adoConnection) : base(adoConnection) { }
12 |
13 | public async Task GetWorkItem(int workItemId)
14 | {
15 | return await GetClient().GetWorkItemAsync(workItemId);
16 | }
17 |
18 | public async Task CreateWorkItem(string projectName, string workItemTitle)
19 | {
20 | var patchDocument = new JsonPatchDocument
21 | {
22 | new JsonPatchOperation()
23 | {
24 | Operation = Operation.Add,
25 | Path = "/fields/System.Title",
26 | Value = workItemTitle
27 | }
28 | };
29 |
30 | return await GetClient().CreateWorkItemAsync(patchDocument, projectName, "Task");
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/AppMenu.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 |
6 | namespace ServicePrincipalsSamples
7 | {
8 | public class AppMenu
9 | {
10 | private static int optionsCounter;
11 |
12 | private readonly List optionGroups = new();
13 | private readonly List allOptions = new();
14 |
15 | private string menuString;
16 |
17 | public async Task DisplayMenu()
18 | {
19 | menuString ??= GenerateMenuAsString();
20 |
21 | Console.Write(menuString);
22 |
23 | try
24 | {
25 | int optionNumber = Convert.ToInt32(Console.ReadLine());
26 | Console.WriteLine();
27 |
28 | if (optionNumber == 0)
29 | {
30 | return false;
31 | }
32 |
33 | var option = allOptions[optionNumber - 1];
34 | await option.Operation();
35 | }
36 | catch (Exception ex) when (ex is FormatException || ex is ArgumentOutOfRangeException)
37 | {
38 | Console.ForegroundColor = ConsoleColor.Red;
39 | Console.WriteLine("Invalid option. Please select one of the options above.");
40 | Console.ResetColor();
41 | }
42 |
43 | return true;
44 | }
45 |
46 | public OptionGroup AddOptionGroup(string name)
47 | {
48 | var group = new OptionGroup(name);
49 | optionGroups.Add(group);
50 | return group;
51 | }
52 |
53 | private string GenerateMenuAsString()
54 | {
55 | var stringBuilder = new StringBuilder();
56 | stringBuilder.AppendLine("\n----------------------------------------------------");
57 | stringBuilder.AppendLine("MENU OPTIONS:\n");
58 | stringBuilder.AppendLine($"0) Exit");
59 |
60 | foreach (var group in optionGroups)
61 | {
62 | stringBuilder.AppendLine($"\n{group.Name}:");
63 |
64 | foreach (var option in group.Options)
65 | {
66 | optionsCounter++;
67 | allOptions.Add(option);
68 | stringBuilder.AppendLine($"{optionsCounter}) {option.Name}");
69 | }
70 | }
71 |
72 | stringBuilder.AppendLine("----------------------------------------------------");
73 | stringBuilder.Append("\nChoose an option: ");
74 |
75 | return stringBuilder.ToString();
76 | }
77 |
78 | public class OptionGroup
79 | {
80 | public string Name { get; }
81 | public List Options { get; } = new List ();
82 |
83 | public OptionGroup(string name)
84 | {
85 | Name = name;
86 | }
87 |
88 | public OptionGroup AddOption(string name, Func operation)
89 | {
90 | Options.Add(new Option(name, operation));
91 | return this;
92 | }
93 | }
94 |
95 | public class Option
96 | {
97 | public string Name { get; }
98 | public Func Operation { get; }
99 |
100 | public Option(string name, Func operation)
101 | {
102 | Name = name;
103 | Operation = operation;
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Net.Mime;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Net.Http.Headers;
7 | using ServicePrincipalsSamples.Aad;
8 | using ServicePrincipalsSamples.AdoClient;
9 | using ServicePrincipalsSamples.Settings;
10 |
11 | namespace ServicePrincipalsSamples
12 | {
13 | static class Program
14 | {
15 | static async Task Main()
16 | {
17 | bool showMenu = true;
18 | while (showMenu)
19 | {
20 | try
21 | {
22 | AdoAuthenticationMode mode = GetAdoAuthenticationMode();
23 | showMenu = false;
24 |
25 | var config = AppConfiguration.ReadFromJsonFile();
26 | config.Ado.AdoAuthenticationMode = mode;
27 |
28 | using var serviceProvider = ConfigureServices(config);
29 | await serviceProvider.GetService().Run();
30 | }
31 | catch (Exception e)
32 | {
33 | Console.ForegroundColor = ConsoleColor.Red;
34 | Console.WriteLine(e.Message);
35 | Console.ResetColor();
36 | }
37 | }
38 | }
39 |
40 | private static AdoAuthenticationMode GetAdoAuthenticationMode()
41 | {
42 | Console.Write("Azure DevOps authentication mode:\n" +
43 | "1. Azure AD Service Principal\n" +
44 | "2. ADO PAT\n" +
45 | "Select (Default: Azure AD Service Principal): ");
46 |
47 | var optionString = Console.ReadLine();
48 |
49 | if (string.IsNullOrEmpty(optionString))
50 | {
51 | return AdoAuthenticationMode.AadServicePrincipal;
52 | }
53 | else if (int.TryParse(optionString, out int optionNumber) && Enum.IsDefined(typeof(AdoAuthenticationMode), optionNumber))
54 | {
55 | return (AdoAuthenticationMode)optionNumber;
56 | }
57 | else
58 | {
59 | throw new InvalidOperationException($"Unsupported authentication mechanism: {optionString}");
60 | }
61 | }
62 |
63 | private static ServiceProvider ConfigureServices(AppConfiguration config)
64 | {
65 | var services = new ServiceCollection();
66 |
67 | services
68 | .AddSingleton(config)
69 | .AddSingleton()
70 | .AddSingleton()
71 | .AddSingleton();
72 |
73 | services
74 | .AddTransient()
75 | .AddTransient();
76 |
77 | // Register Azure DevOps clients
78 | services
79 | .AddTransient()
80 | .AddTransient()
81 | .AddTransient()
82 | .AddTransient();
83 |
84 | // Register simple REST client without client libs
85 | services
86 | .AddHttpClient(client =>
87 | {
88 | client.DefaultRequestHeaders.Add(HeaderNames.Accept, MediaTypeNames.Application.Json);
89 | AppConfiguration.AppUserAgent.ForEach(header => client.DefaultRequestHeaders.UserAgent.Add(header));
90 | })
91 | .AddHttpMessageHandler();
92 |
93 | return services.BuildServiceProvider();
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "ConsoleApp (Default)": {
4 | "commandName": "Project"
5 | },
6 | "ConsoleApp (Dev)": {
7 | "commandName": "Project",
8 | "environmentVariables": {
9 | "ASPNETCORE_ENVIRONMENT": "Dev"
10 | }
11 | },
12 | "ConsoleApp (Prod)": {
13 | "commandName": "Project",
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Prod"
16 | }
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/README.md:
--------------------------------------------------------------------------------
1 | # .NET Core console application using an Azure AD Application to call Azure DevOps REST API
2 |
3 | This sample shows how to get an Azure AD access token for an Application Service Principal (using a secret or a certificate) using [Microsoft Authentication Library for .NET (MSAL.NET)](https://aka.ms/msal-net) and authenticate to Azure DevOps to perform multiple operations. It covers the following topics:
4 |
5 | - Usage comparison between Azure AD Service Principal access tokens and Azure DevOps PATs.
6 | - How to use `VssConnection` following best practises.
7 | - How to use the MSAL in-memory token cache and handle the access token expiration.
8 |
9 | ## How to run this sample
10 |
11 | **Prerequisites**
12 |
13 | - [.NET Core SDK](https://dotnet.microsoft.com/en-us/download) - 6.0 or higher
14 | - [Azure DevOps .NET client libraries](https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/dotnet-client-libraries?view=azure-devops) - 19.219.0-preview or higher
15 | - [Visual Studio / Visual Studio Code](https://aka.ms/vsdownload)
16 |
17 | ### Step 1: Clone or download this repository
18 |
19 | From a shell or command line:
20 |
21 | ```ps
22 | git clone https://github.com/microsoft/azure-devops-auth-samples.git
23 | ```
24 |
25 | ### Step 2: Create an Azure AD application
26 |
27 | In the tenant to which your Azure DevOps organization is connected, [create an Azure AD Application](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
28 |
29 | ### Step 3: Add the Azure AD application to your Azure DevOps Organization
30 |
31 | Once the application is created, [add it to your Azure DevOps organization](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity#step-by-step-configuration).
32 |
33 | ### Step 4: Configure the sample to use your Azure AD application
34 |
35 | Update the configuration file `Settings\appsettings.json` with the information about your Azure AD application and Azure DevOps organization.
36 |
37 | ### Step 5: Run the sample
38 |
39 | **From Visual Studio:**
40 | 1. Open the solution file `../ServicePrincipalsSamples.sln`.
41 | 2. Build the project and run using the profile `ConsoleApp (Default)`.
42 |
43 | -- OR --
44 |
45 | **From the console:**
46 | ```cmd
47 | cd 1-ConsoleApp-AppRegistration
48 | dotnet run
49 | ```
50 |
51 | ## App settings profiles
52 |
53 | You can switch between settings files by using profiles. For example, to use a profile called `Dev`:
54 |
55 | 1. Create a copy of `appsettings.json` and name it `appsettings.Dev.json`. In this sample, non-default profile settings are excluded from Git via `.gitignore`.
56 | 2. Run the app:
57 | - In Visual Studio, run using the profile `ConsoleApp (Dev)` (already configured in this sample). To use a new one you need to add it to `Properties\launchSettings.json`.
58 | - In the console:
59 | ```cmd
60 | cd 1-ConsoleApp-AppRegistration
61 | dotnet run --environment Dev
62 | ```
63 |
64 | ## References
65 |
66 | - [Authentication flow support in MSAL](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#client-credentials)
67 | - [Acquire and cache tokens using the Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens)
68 | - [A .NET Core daemon console application using MSAL.NET to acquire tokens for resources](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2)
69 | - [Use IHttpClientFactory to implement resilient HTTP requests](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests)
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Settings/AadConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Identity.Web;
2 | using System;
3 |
4 | namespace ServicePrincipalsSamples.Settings
5 | {
6 | ///
7 | /// Settings related to Azure Active Directory (Azure AD)
8 | ///
9 | public class AadConfiguration
10 | {
11 | ///
12 | /// instance of Azure AD, for example public Azure or a Sovereign cloud (Azure China, Germany, US government, etc ...)
13 | ///
14 | public string Instance { get; set; }
15 |
16 | ///
17 | /// The Tenant is:
18 | /// - either the tenant ID of the Azure AD tenant in which this application is registered (a guid)
19 | /// or a domain name associated with the tenant
20 | /// - or 'organizations' (for a multi-tenant application)
21 | ///
22 | public string Tenant { get; set; }
23 |
24 | ///
25 | /// Guid used by the application to uniquely identify itself to Azure AD
26 | ///
27 | public string ClientId { get; set; }
28 |
29 | ///
30 | /// URL of the authority.
31 | ///
32 | public Uri Authority
33 | {
34 | get
35 | {
36 | return new Uri(new Uri(Instance), Tenant);
37 | }
38 | }
39 |
40 | ///
41 | /// Client secret (application password)
42 | ///
43 | /// Daemon applications can authenticate with Azure AD through two mechanisms: ClientSecret
44 | /// (which is a kind of application password: this property)
45 | /// or a certificate previously shared with AzureAD during the application registration
46 | /// (and identified by the Certificate property below)
47 | ///
48 | public string ClientSecret { get; set; }
49 |
50 | ///
51 | /// The description of the certificate to be used to authenticate your application.
52 | ///
53 | public CertificateDescription Certificate { get; set; }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Settings/AdoConfiguration.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ServicePrincipalsSamples.Settings
4 | {
5 | public enum AdoAuthenticationMode
6 | {
7 | AadServicePrincipal = 1,
8 | AdoPat = 2
9 | }
10 |
11 | ///
12 | /// Settings related to Azure DevOps
13 | ///
14 | public class AdoConfiguration
15 | {
16 | ///
17 | /// Azure DevOps API version
18 | ///
19 | public const string DefaultApiVersion = "7.1-preview.1";
20 |
21 | public AdoAuthenticationMode AdoAuthenticationMode { get; set; }
22 |
23 | ///
24 | /// Azure DevOps base url
25 | ///
26 | public string Instance { get; set; }
27 |
28 | ///
29 | /// Azure DevOps org name (https://dev.azure.com/{Organization})
30 | ///
31 | public string Organization { get; set; }
32 |
33 | ///
34 | /// Azure DevOps Personal Access Token (PAT)
35 | ///
36 | public string Pat { get; set; }
37 |
38 | ///
39 | /// Organization URL
40 | ///
41 | public Uri OrganizationUrl
42 | {
43 | get
44 | {
45 | return new Uri(new Uri(Instance), Organization);
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Settings/AppConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Net.Http.Headers;
6 |
7 | namespace ServicePrincipalsSamples.Settings
8 | {
9 | public class AppConfiguration
10 | {
11 | ///
12 | /// Custom UserAgent header - "Identity.ServicePrincipalsSamples/1.0 (1-ConsoleApp-AppRegistration)"
13 | ///
14 | public static List AppUserAgent { get; } = new()
15 | {
16 | new ProductInfoHeaderValue("Identity.ServicePrincipalsSamples", "1.0"),
17 | new ProductInfoHeaderValue("(1-ConsoleApp-AppRegistration)")
18 | };
19 |
20 | public AadConfiguration Aad { get; set; }
21 |
22 | public AdoConfiguration Ado { get; set; }
23 |
24 | ///
25 | /// Reads the configuration from a json file
26 | ///
27 | /// Configuration read from the json file
28 | public static AppConfiguration ReadFromJsonFile()
29 | {
30 | var path = "Settings/appsettings.json";
31 | var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
32 |
33 | if (environmentName != null)
34 | {
35 | path = $"Settings/appsettings.{environmentName}.json";
36 | }
37 |
38 | return new ConfigurationBuilder()
39 | .SetBasePath(Directory.GetCurrentDirectory())
40 | .AddJsonFile(path, optional: false, reloadOnChange: true)
41 | .Build().Get();
42 | }
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/Settings/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Aad": {
3 | "Instance": "https://login.microsoftonline.com/{0}",
4 | "Tenant": "[Enter here the tenantID or domain name for your Azure AD tenant]",
5 | "ClientId": "[Enter here the ClientId for your application]",
6 | "ClientSecret": "[Enter here a client secret for your application]",
7 | "Certificate": {}
8 | },
9 | "Ado": {
10 | "Instance": "https://dev.azure.com",
11 | "Organization": "[Enter here the name of your organization (https://dev.azure.com/{Organization})]",
12 | "Pat": "[Enter here an Azure DevOps PAT secret]"
13 | }
14 | }
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/1-ConsoleApp-AppRegistration/TableResult.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Services.Common;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace ServicePrincipalsSamples
8 | {
9 | public class TableResult
10 | {
11 | public IList> Columns { get; } = new List>();
12 |
13 | public class Column
14 | {
15 | public int Padding { get; set; }
16 | public string Header { get; set; }
17 | public Func GetValue { get; set; }
18 |
19 | public Column(int padding, string header, Func getValue)
20 | {
21 | Padding = padding;
22 | Header = header;
23 | GetValue = getValue;
24 | }
25 | }
26 |
27 | public TableResult AddColumn(int padding, string header, Func getValue)
28 | {
29 | Columns.Add(new Column(padding, header, getValue));
30 | return this;
31 | }
32 |
33 | public void Display(IList items)
34 | {
35 | var tableFormatBuilder = new StringBuilder();
36 | var headers = new string[Columns.Count];
37 | var rows = new Dictionary>();
38 |
39 | for (var i = 0; i < Columns.Count; i++)
40 | {
41 | if (i != 0)
42 | {
43 | tableFormatBuilder.Append(' ');
44 | }
45 |
46 | tableFormatBuilder.Append($"{{{i},{Columns[i].Padding}}}");
47 | headers[i] = Columns[i].Header;
48 | for (var j = 0; j < items.Count; j++)
49 | {
50 | rows.GetOrAddValue(j).Add(Columns[i].GetValue(items[j]));
51 | }
52 | }
53 |
54 | var tableFormat = tableFormatBuilder.ToString();
55 | PrintTableHeaders(tableFormat, headers);
56 |
57 | foreach (var row in rows)
58 | {
59 | Console.WriteLine(tableFormat, row.Value.ToArray());
60 | }
61 | }
62 |
63 | private static void PrintTableHeaders(string tableFormat, params string[] headers)
64 | {
65 | var headersString = string.Format(tableFormat, headers);
66 | Console.WriteLine(headersString);
67 | Console.WriteLine(string.Join("", headersString.Select(v => "-")));
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/2-ConsoleApp-ManagedIdentity/2-ConsoleApp-ManagedIdentity.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/2-ConsoleApp-ManagedIdentity/Program.cs:
--------------------------------------------------------------------------------
1 | using Azure.Core;
2 | using Azure.Identity;
3 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
4 | using Microsoft.VisualStudio.Services.Client;
5 | using Microsoft.VisualStudio.Services.WebApi;
6 | using System.Net.Http.Headers;
7 |
8 | namespace ServicePrincipalsSamples
9 | {
10 | public static class Program
11 | {
12 | public const string AdoBaseUrl = "https://dev.azure.com";
13 |
14 | public const string AdoOrgName = "Your organization name";
15 |
16 | public const string AadTenantId = "Your Azure AD tenant id";
17 | // ClientId for User Assigned Managed Identity. Leave null for System Assigned Managed Identity
18 | public const string AadUserAssignedManagedIdentityClientId = null;
19 |
20 | public static List AppUserAgent { get; } = new()
21 | {
22 | new ProductInfoHeaderValue("Identity.ManagedIdentitySamples", "1.0"),
23 | new ProductInfoHeaderValue("(2-ConsoleApp-ManagedIdentity)")
24 | };
25 |
26 | public static async Task Main()
27 | {
28 | Console.Write("Work item ID: ");
29 | int workItemId = Convert.ToInt32(Console.ReadLine());
30 |
31 | var vssConnection = CreateVssConnection();
32 |
33 | var workItemTrackingHttpClient = vssConnection.GetClient();
34 | var workItem = await workItemTrackingHttpClient.GetWorkItemAsync(workItemId);
35 |
36 | Console.WriteLine($"Work Item Title: {workItem.Fields["System.Title"]}");
37 | }
38 |
39 | private static VssConnection CreateVssConnection()
40 | {
41 | // DefaultAzureCredential will use VisualStudioCredentials or other appropriate credentials for local development
42 | // but will use ManagedIdentityCredential when deployed to an Azure Host with Managed Identity enabled.
43 | // https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#defaultazurecredential
44 | var credentials = new VssAzureIdentityCredential(
45 | new DefaultAzureCredential(
46 | new DefaultAzureCredentialOptions
47 | {
48 | TenantId = AadTenantId,
49 | ManagedIdentityClientId = AadUserAssignedManagedIdentityClientId,
50 | ExcludeEnvironmentCredential = true // Excluding because EnvironmentCredential was not using correct identity when running in Visual Studio
51 | }
52 | )
53 | );
54 |
55 | var settings = VssClientHttpRequestSettings.Default.Clone();
56 | settings.UserAgent = AppUserAgent;
57 |
58 | var organizationUrl = new Uri(new Uri(AdoBaseUrl), AdoOrgName);
59 | return new VssConnection(organizationUrl, credentials, settings);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/2-ConsoleApp-ManagedIdentity/README.md:
--------------------------------------------------------------------------------
1 | # .NET Core console application using an Azure AD Managed Identity to get a work item
2 |
3 | This sample shows how to get an Azure AD access token for a Managed Identity using [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet) and authenticate to Azure DevOps to create or get a work item.
4 |
5 | ## How to run this sample
6 |
7 | **Prerequisites**
8 |
9 | - [.NET Core SDK](https://dotnet.microsoft.com/en-us/download) - 6.0 or higher
10 | - [Azure DevOps .NET client libraries](https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/dotnet-client-libraries?view=azure-devops) - 19.219.0-preview or higher
11 | - [Visual Studio / Visual Studio Code](https://aka.ms/vsdownload)
12 |
13 | ### Step 1: Clone or download this repository
14 |
15 | From a shell or command line:
16 |
17 | ```ps
18 | git clone https://github.com/microsoft/azure-devops-auth-samples.git
19 | ```
20 |
21 | ### Step 2: Create an Azure VM with a Managed Identity assigned
22 |
23 | To assign a Managed Identity during the [VM creation](https://learn.microsoft.com/en-us/azure/virtual-machines/windows/quick-create-portal) or to an existing one, see [Configure managed identities for Azure resources on a VM using the Azure portal](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm).
24 |
25 | ### Step 3: Add the Managed Identity to your Azure DevOps Organization
26 |
27 | Once the Managed Identity is created, [add it to your Azure DevOps organization](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity#step-by-step-configuration).
28 |
29 | ### Step 4: Configure the sample to use the Managed Identity
30 |
31 | Update constants in the file `Program.cs` with the information about the Managed Identity and Azure DevOps organization.
32 |
33 | ```cs
34 | public const string AdoOrgName = "Your organization name";
35 |
36 | public const string AadTenantId = "Your Azure AD tenant id";
37 | // ClientId for User Assigned Managed Identity. Leave null for System Assigned Managed Identity
38 | public const string AadUserAssignedManagedIdentityClientId = null;
39 | ```
40 |
41 | ### Step 5: Run the sample
42 |
43 | **Test in dev environment**
44 |
45 | From the console run (your AAD credentials will be used):
46 |
47 | ```cmd
48 | az login
49 | cd 2-ConsoleApp-ManagedIdentity
50 | dotnet run
51 | ```
52 |
53 | **Run in the Azure VM**
54 |
55 | The managed identity assigned to the VM will be used in this case. From the console run:
56 |
57 | ```cmd
58 | cd 2-ConsoleApp-ManagedIdentity
59 | dotnet run
60 | ```
61 |
62 | # References
63 | - [Azure.Identity - DefaultAzureCredential](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#defaultazurecredential)
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/3-AzureFunction-ManagedIdentity/3-AzureFunction-ManagedIdentity.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 | v4
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 | PreserveNewest
18 | Never
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/3-AzureFunction-ManagedIdentity/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "AzureFunctionTest": {
4 | "commandName": "Project",
5 | "commandLineArgs": "--port 7149",
6 | "launchBrowser": false
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/3-AzureFunction-ManagedIdentity/README.md:
--------------------------------------------------------------------------------
1 | # Azure Function using an Azure AD Managed Identity to get a work item
2 |
3 | This sample shows how to get an Azure AD access token for a Managed Identity using [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet) and authenticate to Azure DevOps to create or get a work item.
4 |
5 | ## How to run this sample
6 |
7 | **Prerequisites**
8 |
9 | - [.NET Core SDK](https://dotnet.microsoft.com/en-us/download) - 6.0 or higher
10 | - [Azure DevOps .NET client libraries](https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/dotnet-client-libraries?view=azure-devops) - 19.219.0-preview or higher
11 | - [Visual Studio / Visual Studio Code](https://aka.ms/vsdownload)
12 |
13 | ### Step 1: Clone or download this repository
14 |
15 | From a shell or command line:
16 |
17 | ```ps
18 | git clone https://github.com/microsoft/azure-devops-auth-samples.git
19 | ```
20 |
21 | ### Step 2: Create an Azure Function with a Managed Identity assigned
22 |
23 | 1. To create an Azure Function, see [Create your first function in the Azure portal](https://learn.microsoft.com/en-us/azure/azure-functions/functions-create-function-app-portal).
24 | 2. To assign it a Managed Identiy, see [How to use managed identities for App Service and Azure Functions](https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp).
25 |
26 | ### Step 3: Add the Managed Identity to your Azure DevOps Organization
27 |
28 | Once the Managed Identity is created, [add it to your Azure DevOps organization](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity#step-by-step-configuration).
29 |
30 | ### Step 2: Configure the sample
31 |
32 | Update constants in the file `TestMIHttpTrigger.cs` with the information about your Azure AD Managed Identity and Azure DevOps organization.
33 |
34 | ```cs
35 | public const string AdoOrgName = "Your organization name";
36 |
37 | public const string AadTenantId = "Your Azure AD tenant id";
38 | // ClientId for User Assigned Managed Identity. Leave null for System Assigned Managed Identity
39 | public const string AadUserAssignedManagedIdentityClientId = null;
40 | ```
41 |
42 | ### Step 5: Run the sample
43 |
44 | The sample will use different credentials depending on the environment.
45 |
46 | - **In Azure**, the managed identity will be used.
47 |
48 | **Test in dev environment**
49 |
50 | In Visual Studio:
51 |
52 | 1. Open the solution file `../ServicePrincipalsSamples.sln`.
53 | 2. Configure the Azure account to be used in `Tools -> Options -> Azure Service Authentication -> Account Selection`.
54 | 3. Build the project and run using the profile `AzureFunctionTest`.
55 | 4. In the output you will get the function URL that you can call with the query parameter `?workItemId={put here some work item id}`.
56 |
57 | See [Azure Identity client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#defaultazurecredential) for more details and options for providing local development credentials.
58 |
59 | **Run in the Azure VM**
60 |
61 | Follow the following steps from the guide [Quickstart: Create your first C# function in Azure using Visual Studio](https://learn.microsoft.com/en-us/azure/azure-functions/functions-create-your-first-function-visual-studio?tabs=in-process#publish-the-project-to-azure):
62 |
63 | 1. Publish the project to Azure
64 | 2. Verify your function in Azure
65 |
66 | # References
67 |
68 | - [Introduction to Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview)
69 | - [Azure.Identity - ManagedIdentityCredential.GetTokenAsync Method](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential.gettokenasync?view=azure-dotnet)
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/3-AzureFunction-ManagedIdentity/TestMIHttpTrigger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net.Http.Headers;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Azure.Core;
7 | using Azure.Identity;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 | using Microsoft.Azure.WebJobs;
11 | using Microsoft.Azure.WebJobs.Extensions.Http;
12 | using Microsoft.Extensions.Logging;
13 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
14 | using Microsoft.VisualStudio.Services.Client;
15 | using Microsoft.VisualStudio.Services.WebApi;
16 |
17 | namespace Company.Function
18 | {
19 | public static class TestMIHttpTrigger
20 | {
21 | public const string AdoBaseUrl = "https://dev.azure.com";
22 |
23 | public const string AdoOrgName = "Your organization name";
24 |
25 | public const string AadTenantId = "Your Azure AD tenant id";
26 | // ClientId for User Assigned Managed Identity. Leave null for System Assigned Managed Identity
27 | public const string AadUserAssignedManagedIdentityClientId = null;
28 |
29 | // Credentials object is static so it can be reused across multiple requests. This ensures
30 | // the internal token cache is used which reduces the number of outgoing calls to Azure AD to get tokens.
31 | //
32 | // DefaultAzureCredential will use VisualStudioCredentials or other appropriate credentials for local development
33 | // but will use ManagedIdentityCredential when deployed to an Azure Host with Managed Identity enabled.
34 | // https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#defaultazurecredential
35 | private readonly static TokenCredential credential =
36 | new DefaultAzureCredential(
37 | new DefaultAzureCredentialOptions
38 | {
39 | TenantId = AadTenantId,
40 | ManagedIdentityClientId = AadUserAssignedManagedIdentityClientId,
41 | ExcludeEnvironmentCredential = true // Excluding because EnvironmentCredential was not using correct identity when running in Visual Studio
42 | });
43 |
44 | public static List AppUserAgent { get; } = new()
45 | {
46 | new ProductInfoHeaderValue("Identity.ManagedIdentitySamples", "1.0"),
47 | new ProductInfoHeaderValue("(3-AzureFunction-ManagedIdentity)")
48 | };
49 |
50 | [FunctionName("TestMIHttpTrigger")]
51 | public static async Task Run(
52 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
53 | ILogger log)
54 | {
55 | if (!int.TryParse(req.Query["workItemId"], out int workItemId))
56 | {
57 | return new BadRequestObjectResult($"Invalid Work item ID: {req.Query["workItemId"]}.");
58 | }
59 |
60 | var vssConnection = CreateVssConnection();
61 |
62 | var workItemTrackingHttpClient = vssConnection.GetClient();
63 |
64 | try
65 | {
66 | var workItem = await workItemTrackingHttpClient.GetWorkItemAsync(workItemId);
67 |
68 | workItem.Fields.TryGetValue("System.Title", out var title);
69 | var responseMessage = $"Work item '{title}' fetched. This HTTP triggered function executed successfully.";
70 | return new OkObjectResult(responseMessage);
71 | }
72 | catch (Exception ex)
73 | {
74 | return new ObjectResult(ex.Message);
75 | }
76 | }
77 |
78 | private static VssConnection CreateVssConnection()
79 | {
80 | var credentials = new VssAzureIdentityCredential(credential);
81 |
82 | var settings = VssClientHttpRequestSettings.Default.Clone();
83 | settings.UserAgent = AppUserAgent;
84 |
85 | var organizationUrl = new Uri(new Uri(AdoBaseUrl), AdoOrgName);
86 | return new VssConnection(organizationUrl, credentials, settings);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/3-AzureFunction-ManagedIdentity/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/3-AzureFunction-ManagedIdentity/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "AzureWebJobsStorage": "",
5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet"
6 | }
7 | }
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/README.md:
--------------------------------------------------------------------------------
1 | # Azure AD Service Principals and Managed Identities in Azure DevOps (.NET Core)
2 |
3 | These .NET Core samples show how to use Azure AD Service Principals and Managed Identities to authenticate to Azure DevOps using [Microsoft Authentication Library for .NET (MSAL.NET)](https://aka.ms/msal-net) and the [Azure DevOps .NET client libraries](https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/dotnet-client-libraries?view=azure-devops).
4 |
5 | ## Code samples
6 |
7 | | Project | Description |
8 | |--|--|
9 | | [0-SimpleConsoleApp-AppRegistration](/0-SimpleConsoleApp-AppRegistration/) | It uses an Azure AD Application Service Principal to create get a work item. |
10 | | [1-ConsoleApp-AppRegistration](/1-ConsoleApp-AppRegistration/) | It uses an Azure AD Application Service Principal to perform multiple operations in Azure DevOps. It also shows how to use the MSAL in-memory token cache and handle the access token expiration. |
11 | | [2-ConsoleApp-ManagedIdentity](/2-ConsoleApp-ManagedIdentity/) | It uses an Azure AD Managed Identity to get a work item. |
12 | | [3-AzureFunction-ManagedIdentity](/3-AzureFunction-ManagedIdentity/) | It uses an Azure AD Managed Identity to get a work item using an Azure Function. |
13 |
14 | ## Usage with Azure DevOps .NET client libs
15 |
16 | Learn how to use Azure AD Service Principals with the client libs.
17 |
18 | ### How to replace an Azure DevOps PAT with an Azure AD access token
19 |
20 | Creating Azure DevOps credentials is very similar in both cases:
21 |
22 | ```cs
23 | // Azure DevOps PAT
24 | var credentials = new VssBasicCredential(string.Empty, "pat_secret");
25 |
26 | // Azure AD Service Principal access token
27 | var token = new VssAadToken("Bearer", "aad_access_token");
28 | var credentials = new VssAadCredential(token);
29 | ```
30 |
31 | Then any of them can be used to create an instance of `VssConnection` (remember that this instance should be a singleton in the application):
32 |
33 | ```cs
34 | var organizationUrl = "http://dev.azure.com/Fabrikam";
35 | var vssConnection = new VssConnection(organizationUrl, credentials);
36 | var adoClient = vssConnection.GetClient<_AdoHttpClient_>();
37 | ```
38 |
39 | ### How to regenerate an Azure AD access token using VssConnection
40 |
41 | As Azure AD access tokens are short-lived, you can provide a delegate to `VssAadToken` to acquire a new access token when the existing one expires. [Microsoft Authentication Library for .NET (MSAL.NET)](https://aka.ms/msal-net-authenticationresult) has a token cache and handles automatically the token acquisition when an access token is expired.
42 |
43 | For example, using an Azure AD application with a client secret:
44 |
45 | > **Note:** At the moment of writing this, `VssAadToken` does not support asynchronous delegates
46 |
47 | ```cs
48 | var app = ConfidentialClientApplicationBuilder.Create("client_id")
49 | .WithClientSecret("client_secret")
50 | .WithAuthority("https://login.microsoftonline.com/tenant_id")
51 | .Build();
52 |
53 | // It uses Azure DevOps default scope (499b84ac-1321-427f-aa17-267ca6975798/.default)
54 | var token = new VssAadToken((scopes) => app.AcquireTokenForClient(scopes).ExecuteAsync().SyncResultConfigured());
55 | var credentials = new VssAadCredential(token);
56 | ```
57 |
58 | See [1-ConsoleApp-AppRegistration](/1-ConsoleApp-AppRegistration/) for more information.
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/ClientLibsNET/ServicePrincipalsSamples.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32901.215
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "1-ConsoleApp-AppRegistration", "1-ConsoleApp-AppRegistration\1-ConsoleApp-AppRegistration.csproj", "{EDFAC736-E098-416E-A8EE-54BFA6FFD293}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{89992743-9EFD-4840-87C0-6032DCE1E2B9}"
9 | ProjectSection(SolutionItems) = preProject
10 | README.md = README.md
11 | EndProjectSection
12 | EndProject
13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "0-SimpleConsoleApp-AppRegistration", "0-SimpleConsoleApp-AppRegistration\0-SimpleConsoleApp-AppRegistration.csproj", "{D6F07467-04B7-4BB6-BF08-48BA074F38DC}"
14 | EndProject
15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "2-ConsoleApp-ManagedIdentity", "2-ConsoleApp-ManagedIdentity\2-ConsoleApp-ManagedIdentity.csproj", "{03210CB0-0C27-425F-82DD-CD7AE28DB271}"
16 | EndProject
17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "3-AzureFunction-ManagedIdentity", "3-AzureFunction-ManagedIdentity\3-AzureFunction-ManagedIdentity.csproj", "{BCDE96F6-6934-4104-AF19-99701300E2C4}"
18 | EndProject
19 | Global
20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
21 | Debug|Any CPU = Debug|Any CPU
22 | Release|Any CPU = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {EDFAC736-E098-416E-A8EE-54BFA6FFD293}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {EDFAC736-E098-416E-A8EE-54BFA6FFD293}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {EDFAC736-E098-416E-A8EE-54BFA6FFD293}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {EDFAC736-E098-416E-A8EE-54BFA6FFD293}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {D6F07467-04B7-4BB6-BF08-48BA074F38DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {D6F07467-04B7-4BB6-BF08-48BA074F38DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {D6F07467-04B7-4BB6-BF08-48BA074F38DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {D6F07467-04B7-4BB6-BF08-48BA074F38DC}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {03210CB0-0C27-425F-82DD-CD7AE28DB271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {03210CB0-0C27-425F-82DD-CD7AE28DB271}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {03210CB0-0C27-425F-82DD-CD7AE28DB271}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {03210CB0-0C27-425F-82DD-CD7AE28DB271}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {BCDE96F6-6934-4104-AF19-99701300E2C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {BCDE96F6-6934-4104-AF19-99701300E2C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {BCDE96F6-6934-4104-AF19-99701300E2C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {BCDE96F6-6934-4104-AF19-99701300E2C4}.Release|Any CPU.Build.0 = Release|Any CPU
41 | EndGlobalSection
42 | GlobalSection(SolutionProperties) = preSolution
43 | HideSolutionNode = FALSE
44 | EndGlobalSection
45 | GlobalSection(ExtensibilityGlobals) = postSolution
46 | SolutionGuid = {67C3D201-AA44-4D13-A9FA-1D8989E570F2}
47 | EndGlobalSection
48 | EndGlobal
49 |
--------------------------------------------------------------------------------
/ServicePrincipalsSamples/README.md:
--------------------------------------------------------------------------------
1 | # Azure AD Service Principals and Managed Identities in Azure DevOps
2 |
3 | These samples show how to [use Azure AD Service Principals and Managed Identities to authenticate to Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity) using OAuth 2.0 client credentials flow in different scenarios.
4 |
5 | ## Code samples
6 |
7 | | Subfolder | Description |
8 | |--|--|
9 | | [ClientLibsNET](/ServicePrincipalsSamples/ClientLibsNET/) | .NET Core samples using [Azure DevOps .NET client libraries](https://learn.microsoft.com/en-us/azure/devops/integrate/concepts/dotnet-client-libraries?view=azure-devops) with Azure AD Application Service Principals and Managed Identities. |
10 |
11 | ## References
12 |
13 | For more information about Azure DevOps:
14 |
15 | - [Use Azure AD service principals & managed identities in Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity)
16 | - [Azure DevOps Services REST API Reference](https://learn.microsoft.com/en-us/rest/api/azure/devops)
17 |
18 | For more information about Azure AD Service Principals:
19 |
20 | - Azure AD Applications - [Application and service principal objects in Azure Active Directory](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals)
21 | - Azure AD Managed Identitis - [What are managed identities for Azure resources?](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview)
22 | - Authentication protocol - [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://learn.microsoft.com/en-gb/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow)
23 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Microsoft
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 |
--------------------------------------------------------------------------------